Skip to content

Commit

Permalink
Merge pull request #1721 from bugsnag/release/v5.25.0
Browse files Browse the repository at this point in the history
v5.25.0
  • Loading branch information
lemnik authored Jul 19, 2022
2 parents 0cb3c55 + 6ab443a commit b09e46b
Show file tree
Hide file tree
Showing 33 changed files with 333 additions and 224 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 5.25.0 (2022-07-19)

### Enhancements

* Feature flags can now be accessed in the onSend and onError callbacks
[#1720](https://github.com/bugsnag/bugsnag-android/pull/1720)
* Feature flags are now kept in and trimmed in order of insertion or modification rather than in alphabetical order
[#1718](https://github.com/bugsnag/bugsnag-android/pull/1718)

## 5.24.0 (2022-06-30)

### Enhancements
Expand Down
10 changes: 10 additions & 0 deletions bugsnag-android-core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$299</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$429</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$499</ID>
<ID>MagicNumber:JsonHelper.kt$JsonHelper$0xff</ID>
<ID>MagicNumber:JsonHelper.kt$JsonHelper$1000</ID>
<ID>MagicNumber:JsonHelper.kt$JsonHelper$16</ID>
<ID>MagicNumber:JsonHelper.kt$JsonHelper$18</ID>
<ID>MagicNumber:JsonHelper.kt$JsonHelper$19</ID>
<ID>MagicNumber:JsonHelper.kt$JsonHelper$3</ID>
<ID>MagicNumber:JsonHelper.kt$JsonHelper$8</ID>
<ID>MagicNumber:LastRunInfoStore.kt$LastRunInfoStore$3</ID>
<ID>MaxLineLength:LastRunInfo.kt$LastRunInfo$return "LastRunInfo(consecutiveLaunchCrashes=$consecutiveLaunchCrashes, crashed=$crashed, crashedDuringLaunch=$crashedDuringLaunch)"</ID>
<ID>MaxLineLength:ThreadState.kt$ThreadState$"[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]"</ID>
<ID>NestedBlockDepth:JsonHelper.kt$JsonHelper$ fun jsonToLong(value: Any?): Long?</ID>
<ID>ProtectedMemberInFinalClass:ConfigInternal.kt$ConfigInternal$protected val plugins = HashSet&lt;Plugin>()</ID>
<ID>ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun isAnr(event: Event): Boolean</ID>
<ID>ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun shouldDiscardClass(): Boolean</ID>
Expand All @@ -39,7 +47,9 @@
<ID>SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get battery status") }</ID>
<ID>SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get locationStatus") }</ID>
<ID>SwallowedException:DeviceIdFilePersistence.kt$DeviceIdFilePersistence$catch (exc: OverlappingFileLockException) { Thread.sleep(FILE_LOCK_WAIT_MS) }</ID>
<ID>SwallowedException:JsonHelperTest.kt$JsonHelperTest$catch (e: IllegalArgumentException) { didThrow = true }</ID>
<ID>SwallowedException:PluginClient.kt$PluginClient$catch (exc: ClassNotFoundException) { logger.d("Plugin '$clz' is not on the classpath - functionality will not be enabled.") null }</ID>
<ID>ThrowsCount:JsonHelper.kt$JsonHelper$ fun jsonToLong(value: Any?): Long?</ID>
<ID>TooManyFunctions:ConfigInternal.kt$ConfigInternal : CallbackAwareMetadataAwareUserAwareFeatureFlagAware</ID>
<ID>TooManyFunctions:DeviceDataCollector.kt$DeviceDataCollector</ID>
<ID>TooManyFunctions:EventInternal.kt$EventInternal : FeatureFlagAwareStreamableMetadataAwareUserAware</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,31 +184,7 @@ internal class BugsnagEventMapper(
}

internal fun convertStacktrace(trace: List<Map<String, Any?>>): Stacktrace {
return Stacktrace(trace.map { convertStackframe(it) })
}

internal fun convertStackframe(frame: Map<String, Any?>): Stackframe {
val copy: MutableMap<String, Any?> = frame.toMutableMap()
val lineNumber = frame["lineNumber"] as? Number
copy["lineNumber"] = lineNumber?.toLong()

(frame["frameAddress"] as? String)?.let {
copy["frameAddress"] = java.lang.Long.decode(it)
}

(frame["symbolAddress"] as? String)?.let {
copy["symbolAddress"] = java.lang.Long.decode(it)
}

(frame["loadAddress"] as? String)?.let {
copy["loadAddress"] = java.lang.Long.decode(it)
}

(frame["isPC"] as? Boolean)?.let {
copy["isPC"] = it
}

return Stackframe(copy)
return Stacktrace(trace.map { Stackframe(it) })
}

internal fun deserializeSeverityReason(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ public List<Breadcrumb> getBreadcrumbs() {
return impl.getBreadcrumbs();
}

/**
* A list of feature flags active at the time of the event.
* See {@link FeatureFlag} for details of the data available.
*/
@NonNull
public List<FeatureFlag> getFeatureFlags() {
return impl.getFeatureFlags().toList();
}

/**
* Information set by the notifier about your app can be found in this field. These values
* can be accessed and amended if necessary.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
package com.bugsnag.android

import java.io.IOException
import java.util.concurrent.ConcurrentHashMap

internal class FeatureFlags(
internal val store: MutableMap<String, String?> = ConcurrentHashMap()
internal val store: MutableMap<String, String?> = mutableMapOf()
) : JsonStream.Streamable, FeatureFlagAware {
private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__"

override fun addFeatureFlag(name: String) {
store[name] = emptyVariant
@Synchronized override fun addFeatureFlag(name: String) {
addFeatureFlag(name, null)
}

override fun addFeatureFlag(name: String, variant: String?) {
@Synchronized override fun addFeatureFlag(name: String, variant: String?) {
store.remove(name)
store[name] = variant ?: emptyVariant
}

override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
@Synchronized override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
featureFlags.forEach { (name, variant) ->
addFeatureFlag(name, variant)
}
}

override fun clearFeatureFlag(name: String) {
@Synchronized override fun clearFeatureFlag(name: String) {
store.remove(name)
}

override fun clearFeatureFlags() {
@Synchronized override fun clearFeatureFlags() {
store.clear()
}

@Throws(IOException::class)
override fun toStream(stream: JsonStream) {
val storeCopy = synchronized(this) { store.toMap() }
stream.beginArray()
store.forEach { (name, variant) ->
storeCopy.forEach { (name, variant) ->
stream.beginObject()
stream.name("featureFlag").value(name)
if (variant != emptyVariant) {
Expand All @@ -44,9 +45,9 @@ internal class FeatureFlags(
stream.endArray()
}

fun toList(): List<FeatureFlag> = store.entries.map { (name, variant) ->
@Synchronized fun toList(): List<FeatureFlag> = store.entries.map { (name, variant) ->
FeatureFlag(name, variant.takeUnless { it == emptyVariant })
}

fun copy() = FeatureFlags(store.toMutableMap())
@Synchronized fun copy() = FeatureFlags(store.toMutableMap())
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bugsnag.android

import com.bugsnag.android.internal.JsonHelper
import java.io.IOException

/**
Expand Down Expand Up @@ -59,9 +60,9 @@ class NativeStackframe internal constructor(
writer.name("method").value(method)
writer.name("file").value(file)
writer.name("lineNumber").value(lineNumber)
writer.name("frameAddress").value(frameAddress)
writer.name("symbolAddress").value(symbolAddress)
writer.name("loadAddress").value(loadAddress)
frameAddress?.let { writer.name("frameAddress").value(JsonHelper.ulongToHex(frameAddress)) }
symbolAddress?.let { writer.name("symbolAddress").value(JsonHelper.ulongToHex(symbolAddress)) }
loadAddress?.let { writer.name("loadAddress").value(JsonHelper.ulongToHex(loadAddress)) }
writer.name("codeIdentifier").value(codeIdentifier)
writer.name("isPC").value(isPC)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import java.io.IOException
*/
class Notifier @JvmOverloads constructor(
var name: String = "Android Bugsnag Notifier",
var version: String = "5.24.0",
var version: String = "5.25.0",
var url: String = "https://bugsnag.com"
) : JsonStream.Streamable {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bugsnag.android

import com.bugsnag.android.internal.JsonHelper
import java.io.IOException

/**
Expand Down Expand Up @@ -103,13 +104,13 @@ class Stackframe : JsonStream.Streamable {
internal constructor(json: Map<String, Any?>) {
method = json["method"] as? String
file = json["file"] as? String
lineNumber = json["lineNumber"] as? Number
lineNumber = JsonHelper.jsonToLong(json["lineNumber"])
inProject = json["inProject"] as? Boolean
columnNumber = json["columnNumber"] as? Number
frameAddress = (json["frameAddress"] as? Number)?.toLong()
symbolAddress = (json["symbolAddress"] as? Number)?.toLong()
loadAddress = (json["loadAddress"] as? Number)?.toLong()
codeIdentifier = (json["codeIdentifier"] as? String)
frameAddress = JsonHelper.jsonToLong(json["frameAddress"])
symbolAddress = JsonHelper.jsonToLong(json["symbolAddress"])
loadAddress = JsonHelper.jsonToLong(json["loadAddress"])
codeIdentifier = json["codeIdentifier"] as? String
isPC = json["isPC"] as? Boolean

@Suppress("UNCHECKED_CAST")
Expand All @@ -128,9 +129,9 @@ class Stackframe : JsonStream.Streamable {

writer.name("columnNumber").value(columnNumber)

frameAddress?.let { writer.name("frameAddress").value(it) }
symbolAddress?.let { writer.name("symbolAddress").value(it) }
loadAddress?.let { writer.name("loadAddress").value(it) }
frameAddress?.let { writer.name("frameAddress").value(JsonHelper.ulongToHex(frameAddress)) }
symbolAddress?.let { writer.name("symbolAddress").value(JsonHelper.ulongToHex(symbolAddress)) }
loadAddress?.let { writer.name("loadAddress").value(JsonHelper.ulongToHex(loadAddress)) }
codeIdentifier?.let { writer.name("codeIdentifier").value(it) }
isPC?.let { writer.name("isPC").value(it) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,68 @@ internal object JsonHelper {
throw IOException("Could not deserialize from $file", ex)
}
}

/**
* Convert a long that technically contains an unsigned long value into its (unsigned) hex string equivalent.
* Negative values are interpreted as if the sign bit is the high bit of an unsigned integer.
*
* Returns null if null is passed in.
*/
fun ulongToHex(value: Long?): String? {
return if (value == null) {
null
} else if (value >= 0) {
"0x%x".format(value)
} else {
return "0x%x%02x".format(value.ushr(8), value.and(0xff))
}
}

/**
* Convert a JSON-decoded value into a long. Accepts numeric types, or numeric encoded strings
* (e.g. "1234", "0xb1ff").
*
* Returns null if null or an empty string is passed in.
*/
fun jsonToLong(value: Any?): Long? {
return when (value) {
null -> null
is Number -> value.toLong()
is String -> {
if (value.length == 0) {
null
} else {
try {
java.lang.Long.decode(value)
} catch (e: NumberFormatException) {
// Check if the value overflows a long, and correct for it.
if (value.startsWith("0x")) {
// All problematic hex values (e.g. 0x8000000000000000) have 18 characters
if (value.length != 18) {
throw e
}
// Decode all but the last byte, then shift and add it.
// This overflows and gives the "correct" signed result.
val headLength = value.length - 2
java.lang.Long.decode(value.substring(0, headLength))
.shl(8)
.or(value.substring(headLength, value.length).toLong(16))
} else {
// The first problematic decimal value (9223372036854775808) has 19 digits
if (value.length < 19) {
throw e
}
// Decode all but the last 3 chars, then multiply and add them.
// This overflows and gives the "correct" signed result.
val headLength = value.length - 3
java.lang.Long.decode(value.substring(0, headLength)) *
1000 +
java.lang.Long.decode(value.substring(headLength, value.length))
}
}
}
}
else -> throw IllegalArgumentException("Cannot convert " + value + " to long")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ internal class FeatureFlagsSerializationTest {
)

private fun basic() = FeatureFlags().apply {
addFeatureFlag("demo_mode")
addFeatureFlag("sample_group", "a")
addFeatureFlag("demo_mode")
addFeatureFlag("view_mode", "modern")
}

private fun overrideVariants() = FeatureFlags().apply {
addFeatureFlag("demo_mode")
addFeatureFlag("sample_group", "a")
addFeatureFlag("demo_mode")
addFeatureFlag("sample_group", "b")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,12 @@ internal class StackframeSerializationTest {
@Parameter
lateinit var testCase: Pair<Stackframe, String>

private val eventMapper = BugsnagEventMapper(NoopLogger)

@Test
fun testJsonSerialisation() = verifyJsonMatches(testCase.first, testCase.second)

@Test
fun testJsonDeserialisation() =
verifyJsonParser(testCase.first, testCase.second) {
eventMapper.convertStackframe(it)
Stackframe(it)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.bugsnag.android.internal

import org.junit.Assert.assertEquals
import org.junit.Test

internal class JsonHelperTest {
fun assertBidirectional(longValue: Long?, stringValue: String?) {
assertEquals(stringValue, JsonHelper.ulongToHex(longValue))
assertEquals(longValue, JsonHelper.jsonToLong(stringValue))
}

fun assertDecode(stringValue: String?, longValue: Long?) {
assertEquals(longValue, JsonHelper.jsonToLong(stringValue))
}

@Test
fun jsonLongConversions() {
assertBidirectional(null, null)
assertBidirectional(0, "0x0")
assertBidirectional(1, "0x1")
assertBidirectional(0x7fffffffffffffff, "0x7fffffffffffffff")
assertBidirectional(-1, "0xffffffffffffffff")
assertBidirectional(-0x7fffffffffffffff, "0x8000000000000001")
assertBidirectional(-0x7fffffffffffffff - 1, "0x8000000000000000")

assertDecode("", null)
assertDecode("0", 0)
assertDecode("1", 1)
assertDecode("-1", -1)
assertDecode("9223372036854775807", 9223372036854775807)
assertDecode("-9223372036854775807", -9223372036854775807)
assertDecode("-9223372036854775808", -9223372036854775807 - 1)
assertDecode("9223372036854775808", -9223372036854775807 - 1)
assertDecode("0x8000000000000000", -9223372036854775807 - 1)
assertDecode("18446744073709551615", -1)
assertDecode("0xffffffffffffffff", -1)

var didThrow = false
try {
JsonHelper.jsonToLong(false)
} catch (e: IllegalArgumentException) {
didThrow = true
} finally {
assert(didThrow, { "Expected to throw IllegalArgumentException" })
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"variant": "a"
},
{
"featureFlag": "view_mode",
"variant": "modern"
"featureFlag": "demo_mode"
},
{
"featureFlag": "demo_mode"
"featureFlag": "view_mode",
"variant": "modern"
}
]
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[
{
"featureFlag": "sample_group",
"variant": "b"
"featureFlag": "demo_mode"
},
{
"featureFlag": "demo_mode"
"featureFlag": "sample_group",
"variant": "b"
}
]
Loading

0 comments on commit b09e46b

Please sign in to comment.