Updated Bugsnag

This commit is contained in:
M66B 2024-04-29 11:37:18 +02:00
parent caab02b0d6
commit 3989504073
15 changed files with 258 additions and 51 deletions

View File

@ -575,7 +575,7 @@ dependencies {
def minidns_version = "1.0.5"
def openpgp_version = "12.0"
def badge_version = "1.1.22"
def bugsnag_version = "6.1.0"
def bugsnag_version = "6.4.0"
def biweekly_version = "0.6.8"
def vcard_version = "0.12.1"
def relinker_version = "1.4.5"

View File

@ -2,11 +2,26 @@ package com.bugsnag.android
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_EMPTY
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
import android.app.ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE
import android.app.ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Process
import android.os.SystemClock
import com.bugsnag.android.internal.ImmutableConfig
@ -49,12 +64,57 @@ internal class AppDataCollector(
)
}
@SuppressLint("SwitchIntDef")
@Suppress("DEPRECATION")
private fun getProcessImportance(): String? {
try {
val appInfo = ActivityManager.RunningAppProcessInfo()
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
ActivityManager.getMyMemoryState(appInfo)
} else {
val expectedPid = Process.myPid()
activityManager?.runningAppProcesses
?.find { it.pid == expectedPid }
?.let {
appInfo.importance = it.importance
appInfo.pid = expectedPid
}
}
if (appInfo.pid == 0) {
return null
}
return when (appInfo.importance) {
IMPORTANCE_FOREGROUND -> "foreground"
IMPORTANCE_FOREGROUND_SERVICE -> "foreground service"
IMPORTANCE_TOP_SLEEPING -> "top sleeping"
IMPORTANCE_TOP_SLEEPING_PRE_28 -> "top sleeping"
IMPORTANCE_VISIBLE -> "visible"
IMPORTANCE_PERCEPTIBLE -> "perceptible"
IMPORTANCE_PERCEPTIBLE_PRE_26 -> "perceptible"
IMPORTANCE_CANT_SAVE_STATE -> "can't save state"
IMPORTANCE_CANT_SAVE_STATE_PRE_26 -> "can't save state"
IMPORTANCE_SERVICE -> "service"
IMPORTANCE_CACHED -> "cached/background"
IMPORTANCE_GONE -> "gone"
IMPORTANCE_EMPTY -> "empty"
REASON_PROVIDER_IN_USE -> "provider in use"
REASON_SERVICE_IN_USE -> "service in use"
else -> "unknown importance (${appInfo.importance})"
}
} catch (e: Exception) {
return null
}
}
fun getAppDataMetadata(): MutableMap<String, Any?> {
val map = HashMap<String, Any?>()
map["name"] = appName
map["activeScreen"] = sessionTracker.contextActivity
map["lowMemory"] = memoryTrimState.isLowMemory
map["memoryTrimLevel"] = memoryTrimState.trimLevelDescription
map["processImportance"] = getProcessImportance()
populateRuntimeMemoryMetadata(map)
@ -128,6 +188,7 @@ internal class AppDataCollector(
packageManager != null && copy != null -> {
packageManager.getApplicationLabel(copy).toString()
}
else -> null
}
}
@ -156,6 +217,7 @@ internal class AppDataCollector(
VERSION.SDK_INT >= VERSION_CODES.P -> {
Application.getProcessName()
}
else -> {
// see https://stackoverflow.com/questions/19631894
val clz = Class.forName("android.app.ActivityThread")
@ -179,5 +241,7 @@ internal class AppDataCollector(
* good approximation for how long the app has been running.
*/
fun getDurationMs(): Long = SystemClock.elapsedRealtime() - startTimeMs
private const val IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170
}
}

View File

@ -29,12 +29,14 @@ internal class BugsnagEventMapper(
event.userImpl = convertUser(map.readEntry("user"))
// populate metadata
val metadataMap: Map<String, Map<String, Any?>> = map.readEntry("metaData")
val metadataMap: Map<String, Map<String, Any?>> =
(map["metaData"] as? Map<String, Map<String, Any?>>).orEmpty()
metadataMap.forEach { (key, value) ->
event.addMetadata(key, value)
}
val featureFlagsList: List<Map<String, Any?>> = map.readEntry("featureFlags")
val featureFlagsList: List<Map<String, Any?>> =
(map["featureFlags"] as? List<Map<String, Any?>>).orEmpty()
featureFlagsList.forEach { featureFlagMap ->
event.addFeatureFlag(
featureFlagMap.readEntry("featureFlag"),
@ -43,7 +45,8 @@ internal class BugsnagEventMapper(
}
// populate breadcrumbs
val breadcrumbList: List<MutableMap<String, Any?>> = map.readEntry("breadcrumbs")
val breadcrumbList: List<MutableMap<String, Any?>> =
(map["breadcrumbs"] as? List<MutableMap<String, Any?>>).orEmpty()
breadcrumbList.mapTo(event.breadcrumbs) {
Breadcrumb(
convertBreadcrumbInternal(it),
@ -226,8 +229,7 @@ internal class BugsnagEventMapper(
is T -> return value
null -> throw IllegalStateException("cannot find json property '$key'")
else -> throw IllegalArgumentException(
"json property '$key' not " +
"of expected type, found ${value.javaClass.name}"
"json property '$key' not of expected type, found ${value.javaClass.name}"
)
}
}

View File

@ -25,6 +25,7 @@ internal class ConfigInternal(
var releaseStage: String? = null
var sendThreads: ThreadSendPolicy = ThreadSendPolicy.ALWAYS
var persistUser: Boolean = true
var generateAnonymousId: Boolean = true
var launchDurationMillis: Long = DEFAULT_LAUNCH_CRASH_THRESHOLD_MS

View File

@ -178,6 +178,26 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
impl.setPersistUser(persistUser);
}
/**
* Set whether or not Bugsnag should generate an anonymous ID and persist it in local storage
*
* If disabled, any device ID that has been persisted will not be retrieved, and no new
* device ID will be generated or stored
*/
public boolean getGenerateAnonymousId() {
return impl.getGenerateAnonymousId();
}
/**
* Set whether or not Bugsnag should generate an anonymous ID and persist it in local storage
*
* If disabled, any device ID that has been persisted will not be retrieved, and no new
* device ID will be generated or stored
*/
public void setGenerateAnonymousId(boolean generateAnonymousId) {
impl.setGenerateAnonymousId(generateAnonymousId);
}
/**
* Sets the directory where event and session JSON payloads should be persisted if a network
* request is not successful. If you use Bugsnag in multiple processes, then a unique

View File

@ -1,6 +1,7 @@
package com.bugsnag.android
import android.content.Context
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File
import java.util.UUID
@ -8,18 +9,20 @@ import java.util.UUID
* This class is responsible for persisting and retrieving the device ID and internal device ID,
* which uniquely identify this device in various contexts.
*/
internal class DeviceIdStore @JvmOverloads constructor(
internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constructor(
context: Context,
deviceIdfile: File = File(context.filesDir, "device-id"),
deviceIdGenerator: () -> UUID = { UUID.randomUUID() },
internalDeviceIdfile: File = File(context.filesDir, "internal-device-id"),
internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() },
private val sharedPrefMigrator: SharedPrefMigrator,
config: ImmutableConfig,
logger: Logger
) {
private val persistence: DeviceIdPersistence
private val internalPersistence: DeviceIdPersistence
private val generateId = config.generateAnonymousId
init {
persistence = DeviceIdFilePersistence(deviceIdfile, deviceIdGenerator, logger)
@ -35,6 +38,12 @@ internal class DeviceIdStore @JvmOverloads constructor(
* be used. If no value is present then a random UUID will be generated and persisted.
*/
fun loadDeviceId(): String? {
// If generateAnonymousId = false, return null
// so that a previously persisted device ID is not returned,
// or a new one is not generated and persisted
if (!generateId) {
return null
}
var result = persistence.loadDeviceId(false)
if (result != null) {
return result
@ -47,6 +56,12 @@ internal class DeviceIdStore @JvmOverloads constructor(
}
fun loadInternalDeviceId(): String? {
// If generateAnonymousId = false, return null
// so that a previously persisted device ID is not returned,
// or a new one is not generated and persisted
if (!generateId) {
return null
}
return internalPersistence.loadDeviceId(true)
}
}

View File

@ -1,42 +1,113 @@
package com.bugsnag.android
import java.io.IOException
import kotlin.math.max
internal class FeatureFlags(
internal val store: MutableMap<String, String?> = mutableMapOf()
internal class FeatureFlags private constructor(
@Volatile
private var flags: Array<FeatureFlag>
) : JsonStream.Streamable, FeatureFlagAware {
private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__"
@Synchronized override fun addFeatureFlag(name: String) {
/*
* Implemented as *effectively* a CopyOnWriteArrayList - but since FeatureFlags are
* key/value pairs, CopyOnWriteArrayList would require external locking (in addition to it's
* internal locking) for us to be sure we are not adding duplicates.
*
* This class aims to have similar performance while also ensuring that the FeatureFlag object
* themselves don't leak, as they are mutable and we want 'copy' to be an O(1) snapshot
* operation for when an Event is created.
*
* It's assumed that *most* FeatureFlags will be added up-front, or during the normal app
* lifecycle (not during an Event).
*
* As such a copy-on-write structure allows an Event to simply capture a reference to the
* "snapshot" of FeatureFlags that were active when the Event was created.
*/
constructor() : this(emptyArray<FeatureFlag>())
override fun addFeatureFlag(name: String) {
addFeatureFlag(name, null)
}
@Synchronized override fun addFeatureFlag(name: String, variant: String?) {
store[name] = variant ?: emptyVariant
}
override fun addFeatureFlag(name: String, variant: String?) {
synchronized(this) {
val flagArray = flags
val index = flagArray.indexOfFirst { it.name == name }
@Synchronized override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
featureFlags.forEach { (name, variant) ->
addFeatureFlag(name, variant)
flags = when {
// this is a new FeatureFlag
index == -1 -> flagArray + FeatureFlag(name, variant)
// this is a change to an existing FeatureFlag
flagArray[index].variant != variant -> flagArray.copyOf().also {
// replace the existing FeatureFlag in-place
it[index] = FeatureFlag(name, variant)
}
// no actual change, so we return
else -> return
}
}
}
@Synchronized override fun clearFeatureFlag(name: String) {
store.remove(name)
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
synchronized(this) {
val flagArray = flags
val newFlags = ArrayList<FeatureFlag>(
// try to guess a reasonable upper-bound for the output array
if (featureFlags is Collection<*>) flagArray.size + featureFlags.size
else max(flagArray.size * 2, flagArray.size)
)
newFlags.addAll(flagArray)
featureFlags.forEach { (name, variant) ->
val existingIndex = newFlags.indexOfFirst { it.name == name }
when (existingIndex) {
// add a new flag to the end of the list
-1 -> newFlags.add(FeatureFlag(name, variant))
// replace the existing flag
else -> newFlags[existingIndex] = FeatureFlag(name, variant)
}
}
flags = newFlags.toTypedArray()
}
}
@Synchronized override fun clearFeatureFlags() {
store.clear()
override fun clearFeatureFlag(name: String) {
synchronized(this) {
val flagArray = flags
val index = flagArray.indexOfFirst { it.name == name }
if (index == -1) {
return
}
val out = arrayOfNulls<FeatureFlag>(flagArray.size - 1)
flagArray.copyInto(out, 0, 0, index)
flagArray.copyInto(out, index, index + 1)
@Suppress("UNCHECKED_CAST")
flags = out as Array<FeatureFlag>
}
}
override fun clearFeatureFlags() {
synchronized(this) {
flags = emptyArray()
}
}
@Throws(IOException::class)
override fun toStream(stream: JsonStream) {
val storeCopy = synchronized(this) { store.toMap() }
val storeCopy = flags
stream.beginArray()
storeCopy.forEach { (name, variant) ->
stream.beginObject()
stream.name("featureFlag").value(name)
if (variant != emptyVariant) {
if (variant != null) {
stream.name("variant").value(variant)
}
stream.endObject()
@ -44,9 +115,7 @@ internal class FeatureFlags(
stream.endArray()
}
@Synchronized fun toList(): List<FeatureFlag> = store.entries.map { (name, variant) ->
FeatureFlag(name, variant.takeUnless { it == emptyVariant })
}
fun toList(): List<FeatureFlag> = flags.map { (name, variant) -> FeatureFlag(name, variant) }
@Synchronized fun copy() = FeatureFlags(store.toMutableMap())
fun copy() = FeatureFlags(flags)
}

View File

@ -145,6 +145,7 @@ class JsonWriter implements Closeable, Flushable {
*/
private static final String[] REPLACEMENT_CHARS;
private static final String[] HTML_SAFE_REPLACEMENT_CHARS;
static {
REPLACEMENT_CHARS = new String[128];
for (int i = 0; i <= 0x1f; i++) {
@ -165,11 +166,14 @@ class JsonWriter implements Closeable, Flushable {
HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027";
}
/** The output data, containing at most one top-level array or object. */
/**
* The output data, containing at most one top-level array or object.
*/
private final Writer out;
private int[] stack = new int[32];
private int stackSize = 0;
{
push(EMPTY_DOCUMENT);
}
@ -337,7 +341,7 @@ class JsonWriter implements Closeable, Flushable {
* given bracket.
*/
private JsonWriter close(int empty, int nonempty, String closeBracket)
throws IOException {
throws IOException {
int context = peek();
if (context != nonempty && context != empty) {
throw new IllegalStateException("Nesting problem.");
@ -437,7 +441,7 @@ class JsonWriter implements Closeable, Flushable {
}
writeDeferredName();
beforeValue();
out.append(value);
out.write(value);
return this;
}
@ -490,17 +494,18 @@ class JsonWriter implements Closeable, Flushable {
/**
* Encodes {@code value}.
*
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
* {@link Double#isInfinite() infinities}.
* @param value a finite value.
* @return this writer.
*/
public JsonWriter value(double value) throws IOException {
writeDeferredName();
if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
// omit these values instead of attempting to write them
deferredName = null;
} else {
writeDeferredName();
beforeValue();
out.write(Double.toString(value));
}
beforeValue();
out.append(Double.toString(value));
return this;
}
@ -520,7 +525,7 @@ class JsonWriter implements Closeable, Flushable {
* Encodes {@code value}.
*
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
* {@link Double#isInfinite() infinities}.
* {@link Double#isInfinite() infinities}.
* @return this writer.
*/
public JsonWriter value(Number value) throws IOException {
@ -528,14 +533,16 @@ class JsonWriter implements Closeable, Flushable {
return nullValue();
}
writeDeferredName();
String string = value.toString();
if (!lenient
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
// omit this value
deferredName = null;
} else {
writeDeferredName();
beforeValue();
out.write(string);
}
beforeValue();
out.append(string);
return this;
}
@ -634,7 +641,7 @@ class JsonWriter implements Closeable, Flushable {
case NONEMPTY_DOCUMENT:
if (!lenient) {
throw new IllegalStateException(
"JSON must have only one top-level value.");
"JSON must have only one top-level value.");
}
// fall-through
case EMPTY_DOCUMENT: // first in document
@ -647,12 +654,12 @@ class JsonWriter implements Closeable, Flushable {
break;
case NONEMPTY_ARRAY: // another in array
out.append(',');
out.write(',');
newline();
break;
case DANGLING_NAME: // value for name
out.append(separator);
out.write(separator);
replaceTop(NONEMPTY_OBJECT);
break;

View File

@ -20,6 +20,7 @@ internal class ManifestConfigLoader {
private const val AUTO_DETECT_ERRORS = "$BUGSNAG_NS.AUTO_DETECT_ERRORS"
private const val PERSIST_USER = "$BUGSNAG_NS.PERSIST_USER"
private const val SEND_THREADS = "$BUGSNAG_NS.SEND_THREADS"
private const val GENERATE_ANONYMOUS_ID = "$BUGSNAG_NS.GENERATE_ANONYMOUS_ID"
// endpoints
private const val ENDPOINT_NOTIFY = "$BUGSNAG_NS.ENDPOINT_NOTIFY"
@ -108,6 +109,7 @@ internal class ManifestConfigLoader {
autoTrackSessions = data.getBoolean(AUTO_TRACK_SESSIONS, autoTrackSessions)
autoDetectErrors = data.getBoolean(AUTO_DETECT_ERRORS, autoDetectErrors)
persistUser = data.getBoolean(PERSIST_USER, persistUser)
generateAnonymousId = data.getBoolean(GENERATE_ANONYMOUS_ID, generateAnonymousId)
val str = data.getString(SEND_THREADS)

View File

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

View File

@ -11,9 +11,11 @@ internal class ObjectJsonStreamer {
companion object {
internal const val REDACTED_PLACEHOLDER = "[REDACTED]"
internal const val OBJECT_PLACEHOLDER = "[OBJECT]"
internal val DEFAULT_REDACTED_KEYS = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE))
}
var redactedKeys = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE))
var redactedKeys = DEFAULT_REDACTED_KEYS
// Write complex/nested values to a JsonStreamer
@Throws(IOException::class)

View File

@ -36,6 +36,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
private volatile Session currentSession = null;
final BackgroundTaskService backgroundTaskService;
final Logger logger;
private boolean shouldSuppressFirstAutoSession = false;
SessionTracker(ImmutableConfig configuration,
CallbackState callbackState,
@ -76,7 +77,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
@VisibleForTesting
Session startNewSession(@NonNull Date date, @Nullable User user,
boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
if (shouldDiscardSession(autoCaptured)) {
return null;
}
String id = UUID.randomUUID().toString();
@ -92,12 +93,29 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
}
Session startSession(boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
if (shouldDiscardSession(autoCaptured)) {
return null;
}
return startNewSession(new Date(), client.getUser(), autoCaptured);
}
private boolean shouldDiscardSession(boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
return true;
} else {
Session existingSession = currentSession;
if (autoCaptured
&& existingSession != null
&& !existingSession.isAutoCaptured()
&& shouldSuppressFirstAutoSession) {
shouldSuppressFirstAutoSession = true;
return true;
}
}
return false;
}
void pauseSession() {
Session session = currentSession;

View File

@ -19,7 +19,8 @@ internal class StorageModule(
DeviceIdStore(
appContext,
sharedPrefMigrator = sharedPrefMigrator,
logger = logger
logger = logger,
config = immutableConfig
)
}

View File

@ -30,7 +30,9 @@ internal class UserStore @JvmOverloads constructor(
* [Configuration.getPersistUser] is true.
*
* If no user is stored on disk, then a default [User] is used which uses the device ID
* as its ID.
* as its ID (unless the generateAnonymousId config option is set to false, in which case the
* device ID and therefore the user ID is set to
* null).
*
* The [UserState] provides a mechanism for observing value changes to its user property,
* so to avoid interfering with this the method should only be called once for each [Client].
@ -46,6 +48,8 @@ internal class UserStore @JvmOverloads constructor(
val userState = when {
loadedUser != null && validUser(loadedUser) -> UserState(loadedUser)
// if generateAnonymousId config option is false, the deviceId should already be null
// here
else -> UserState(User(deviceId, null, null))
}

View File

@ -57,6 +57,7 @@ data class ImmutableConfig(
val persistenceDirectory: Lazy<File>,
val sendLaunchCrashesSynchronously: Boolean,
val attemptDeliveryOnCrash: Boolean,
val generateAnonymousId: Boolean,
// results cached here to avoid unnecessary lookups in Client.
val packageInfo: PackageInfo?,
@ -166,6 +167,7 @@ internal fun convertToImmutableConfig(
delivery = config.delivery,
endpoints = config.endpoints,
persistUser = config.persistUser,
generateAnonymousId = config.generateAnonymousId,
launchDurationMillis = config.launchDurationMillis,
logger = config.logger!!,
maxBreadcrumbs = config.maxBreadcrumbs,