Skip to content

Commit 1b69fa6

Browse files
authored
Merge pull request #381 from bugsnag/release/v1.14.0
Release v1.14.0
2 parents 97634be + 2bb1650 commit 1b69fa6

File tree

20 files changed

+374
-107
lines changed

20 files changed

+374
-107
lines changed

.github/workflows/codeql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
submodules: recursive
5050
- uses: gradle/wrapper-validation-action@f9c9c575b8b21b6485636a91ffecd10e558c62f6 #v3.5.0
5151

52-
- uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 #v4.7.0
52+
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 #v4.7.1
5353
with:
5454
distribution: 'zulu'
5555
java-version: 17

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
## 1.14.0 (2025-05-12)
2+
3+
### Changes
4+
5+
* Introduced `RemoteSpanContext` to allow cross-layer parenting of spans, along with easy encoding of `traceparent` headers
6+
[#378](https://github.com/bugsnag/bugsnag-android-performance/pull/378)
7+
8+
### Bug fixes
9+
10+
* Corrected the naming of the aggregate CPU metrics attributes
11+
[#380](https://github.com/bugsnag/bugsnag-android-performance/pull/380)
12+
113
## 1.13.0 (2025-04-24)
214

315
### Changes

bugsnag-android-performance/src/main/kotlin/com/bugsnag/android/performance/BugsnagPerformance.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import java.net.URL
3333
* @see [start]
3434
*/
3535
public object BugsnagPerformance {
36-
public const val VERSION: String = "1.13.0"
36+
public const val VERSION: String = "1.14.0"
3737

3838
@get:JvmName("getInstrumentedAppState\$internal")
3939
internal val instrumentedAppState = InstrumentedAppState()
@@ -160,8 +160,10 @@ public object BugsnagPerformance {
160160
}
161161

162162
tracer.sampler = sampler
163+
spanFactory.sampler = sampler
163164
} else {
164165
tracer.sampler = DiscardingSampler
166+
spanFactory.sampler = DiscardingSampler
165167
}
166168

167169
workerTasks.add(SendBatchTask(delivery, tracer, resourceAttributes))
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.bugsnag.android.performance
2+
3+
import com.bugsnag.android.performance.RemoteSpanContext.Companion.encodeAsTraceParent
4+
import com.bugsnag.android.performance.internal.SpanImpl
5+
import com.bugsnag.android.performance.internal.appendHexLong
6+
import com.bugsnag.android.performance.internal.appendHexUUID
7+
import com.bugsnag.android.performance.internal.parseUnsignedLong
8+
9+
import java.util.UUID
10+
11+
/**
12+
* A `SpanContext` implementation that can be constructed from a different system such as a server,
13+
* or a different app layer (such as JavaScript running in a [WebView] or cross-platform framework).
14+
*
15+
* @see [parseTraceParent]
16+
* @see [parseTraceParentOrNull]
17+
*/
18+
public class RemoteSpanContext(
19+
override val spanId: Long,
20+
override val traceId: UUID,
21+
) : SpanContext {
22+
23+
/**
24+
* Returns a string representation of the span context in the W3C `traceparent` header format.
25+
*
26+
* @see [encodeAsTraceParent]
27+
*/
28+
override fun toString(): String {
29+
return encodeAsTraceParent()
30+
}
31+
32+
/**
33+
* Compares this [SpanContext] to another object. Two [SpanContext] objects are considered equal
34+
* if they have the same `spanId` and `traceId`.
35+
*/
36+
override fun equals(other: Any?): Boolean {
37+
return other is SpanContext &&
38+
other.spanId == spanId &&
39+
other.traceId == traceId
40+
}
41+
42+
override fun hashCode(): Int {
43+
return spanId.hashCode() xor traceId.hashCode()
44+
}
45+
46+
public companion object {
47+
private val traceParentRegex =
48+
Regex("^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9]{2}$")
49+
50+
private const val TRACE_ID_MID = 16
51+
private const val TRACE_ID_END = 32
52+
53+
@JvmStatic
54+
public fun encodeAsTraceParent(context: SpanContext): String {
55+
return context.encodeAsTraceParent()
56+
}
57+
58+
/**
59+
* Parses a `traceparent` header string into a [RemoteSpanContext] object. This expects the
60+
* string to be in the W3C traceparent header format defined as part of the
61+
* [W3C Trace Context](https://www.w3.org/TR/trace-context/#traceparent-header)
62+
* specification (version `00`).
63+
*
64+
* Trace flags are currently ignored (but must still be valid).
65+
*
66+
* @throws IllegalArgumentException if the string cannot be parsed
67+
* @see [parseTraceParentOrNull]
68+
*/
69+
@JvmStatic
70+
public fun parseTraceParent(traceParent: String): RemoteSpanContext {
71+
return parseTraceParentOrNull(traceParent)
72+
?: throw IllegalArgumentException("Invalid traceparent string")
73+
}
74+
75+
/**
76+
* Parses a `traceparent` header string into a [RemoteSpanContext] object. This expects the
77+
* string to be in the W3C traceparent header format defined as part of the
78+
* [W3C Trace Context](https://www.w3.org/TR/trace-context/#traceparent-header)
79+
* specification (version `00`).
80+
*
81+
* Trace flags are currently ignored (but must still be valid).
82+
*
83+
* Returns `null` if the string cannot be parsed.
84+
*
85+
* @see [parseTraceParent]
86+
*/
87+
@JvmStatic
88+
public fun parseTraceParentOrNull(traceParent: String): RemoteSpanContext? {
89+
val match = traceParentRegex.matchEntire(traceParent)
90+
?: return null
91+
92+
val (traceIdHex, spanIdHex) = match.destructured
93+
val traceId = UUID(
94+
traceIdHex.substring(0, TRACE_ID_MID).parseUnsignedLong(),
95+
traceIdHex.substring(TRACE_ID_MID, TRACE_ID_END).parseUnsignedLong(),
96+
)
97+
val spanId = spanIdHex.parseUnsignedLong()
98+
99+
return RemoteSpanContext(
100+
spanId = spanId,
101+
traceId = traceId,
102+
)
103+
}
104+
}
105+
}
106+
107+
private const val TRACEPARENT_LENGTH = 55
108+
109+
private fun buildTraceParentHeader(
110+
traceId: UUID,
111+
parentSpanId: Long,
112+
sampled: Boolean,
113+
): String {
114+
return buildString(TRACEPARENT_LENGTH) {
115+
append("00-")
116+
appendHexUUID(traceId)
117+
append('-')
118+
appendHexLong(parentSpanId)
119+
append('-')
120+
append(if (sampled) "01" else "00")
121+
}
122+
}
123+
124+
/**
125+
* Returns a string representation of the span context in the W3C `traceparent` header format.
126+
*/
127+
public fun SpanContext.encodeAsTraceParent(): String {
128+
return buildTraceParentHeader(
129+
traceId = traceId,
130+
parentSpanId = spanId,
131+
sampled = if (this is SpanImpl) isSampled() else true,
132+
)
133+
}

bugsnag-android-performance/src/main/kotlin/com/bugsnag/android/performance/internal/Encodings.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,7 @@ internal fun StringBuilder.appendHexString(bytes: ByteArray): StringBuilder {
4747
bytes.forEach { appendHexPair(it.toInt() and 0xff) }
4848
return this
4949
}
50+
51+
internal fun String.parseUnsignedLong(): Long {
52+
return java.lang.Long.parseUnsignedLong(this, 16)
53+
}

bugsnag-android-performance/src/main/kotlin/com/bugsnag/android/performance/internal/Sampler.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ internal interface Sampler {
88
}
99

1010
fun shouldKeepSpan(span: SpanImpl): Boolean
11+
12+
/**
13+
* The probability that any given [Span] is retained during sampling as a value between 0 and 1.
14+
* Defaults to 1.0 but the implementation should override this value to return an equivalent
15+
* value to the current sampling probability (if possible).
16+
*/
17+
val sampleProbability: Double
18+
get() = 1.0
1119
}
1220

1321
/**
@@ -17,14 +25,15 @@ internal interface Sampler {
1725
internal object DiscardingSampler : Sampler {
1826
override fun sampled(spans: Collection<SpanImpl>): Collection<SpanImpl> = emptyList()
1927
override fun shouldKeepSpan(span: SpanImpl): Boolean = false
28+
override val sampleProbability: Double get() = 0.0
2029
}
2130

2231
internal class ProbabilitySampler(
2332
/**
2433
* The probability that any given [Span] is retained during sampling as a value between 0 and 1.
2534
*/
2635
@FloatRange(from = 0.0, to = 1.0)
27-
var sampleProbability: Double,
36+
override var sampleProbability: Double,
2837
) : Sampler {
2938
// Side effect: Sets span.samplingProbability to the current probability
3039
override fun shouldKeepSpan(span: SpanImpl): Boolean {

bugsnag-android-performance/src/main/kotlin/com/bugsnag/android/performance/internal/SpanFactory.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.bugsnag.android.performance.internal.integration.NotifierIntegration
1616
import com.bugsnag.android.performance.internal.metrics.MetricsContainer
1717
import com.bugsnag.android.performance.internal.processing.AttributeLimits
1818
import com.bugsnag.android.performance.internal.processing.SpanTaskWorker
19+
import com.bugsnag.android.performance.internal.processing.Tracer
1920
import java.util.UUID
2021

2122
internal typealias AttributeSource = (target: SpanImpl) -> Unit
@@ -30,6 +31,8 @@ public class SpanFactory internal constructor(
3031

3132
public var networkRequestCallback: NetworkRequestInstrumentationCallback? = null
3233

34+
internal var sampler: Sampler? = null
35+
3336
internal var attributeLimits: AttributeLimits? = null
3437

3538
public constructor(
@@ -266,6 +269,8 @@ public class SpanFactory internal constructor(
266269
span.attributes["bugsnag.span.first_class"] = isFirstClass
267270
}
268271

272+
span.samplingProbability = sampler?.sampleProbability ?: 1.0
273+
269274
spanAttributeSource(span)
270275

271276
NotifierIntegration.onSpanStarted(span)

bugsnag-android-performance/src/main/kotlin/com/bugsnag/android/performance/internal/metrics/CpuMetricsSource.kt

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -71,45 +71,46 @@ internal class CpuMetricsSource(
7171
var overheadSampleCount = 0
7272

7373
buffer.forEachIndexed(from, to) { index, sample ->
74-
processCpuSamples[index] = sample.processCpuPct
75-
mainThreadCpuSamples[index] = sample.mainCpuPct
76-
overheadCpuSamples[index] = sample.overheadCpuPct
74+
processCpuSamples[index] = sample.processCpuPct.ensurePositive()
75+
mainThreadCpuSamples[index] = sample.mainCpuPct.ensurePositive()
76+
overheadCpuSamples[index] = sample.overheadCpuPct.ensurePositive()
7777
cpuTimestamps[index] = sample.timestamp
7878

79-
if (sample.processCpuPct != -1.0) {
80-
cpuUseTotal += sample.processCpuPct
79+
if (sample.processCpuPct >= 0.0) {
80+
cpuUseTotal += processCpuSamples[index]
8181
cpuUseSampleCount++
8282
}
8383

84-
if (sample.mainCpuPct != -1.0) {
85-
mainThreadCpuTotal += sample.mainCpuPct
84+
if (sample.mainCpuPct >= 0.0) {
85+
mainThreadCpuTotal += mainThreadCpuSamples[index]
8686
mainThreadSampleCount++
8787
}
8888

89-
if (sample.overheadCpuPct != -1.0) {
90-
overheadCpuTotal += sample.overheadCpuPct
89+
if (sample.overheadCpuPct >= 0.0) {
90+
overheadCpuTotal += overheadCpuSamples[index]
9191
overheadSampleCount++
9292
}
9393
}
9494

9595
target.attributes["bugsnag.system.cpu_measures_total"] = processCpuSamples
96-
target.attributes["bugsnag.system.cpu_measures_main_thread"] = mainThreadCpuSamples
97-
target.attributes["bugsnag.system.cpu_measures_overhead"] = overheadCpuSamples
98-
target.attributes["bugsnag.system.cpu_measures_timestamps"] = cpuTimestamps
9996

10097
if (cpuUseSampleCount > 0) {
101-
target.attributes["bugsnag.metrics.cpu_mean_total"] =
102-
cpuUseTotal / cpuUseSampleCount
98+
target.attributes["bugsnag.system.cpu_measures_timestamps"] = cpuTimestamps
99+
target.attributes["bugsnag.system.cpu_mean_total"] =
100+
(cpuUseTotal / cpuUseSampleCount).ensurePositive()
103101
}
104102

105103
if (mainThreadSampleCount > 0) {
106-
target.attributes["bugsnag.metrics.cpu_mean_main_thread"] =
107-
mainThreadCpuTotal / mainThreadSampleCount
104+
target.attributes["bugsnag.system.cpu_measures_main_thread"] =
105+
mainThreadCpuSamples
106+
target.attributes["bugsnag.system.cpu_mean_main_thread"] =
107+
(mainThreadCpuTotal / mainThreadSampleCount).ensurePositive()
108108
}
109109

110110
if (overheadSampleCount > 0) {
111+
target.attributes["bugsnag.system.cpu_measures_overhead"] = overheadCpuSamples
111112
target.attributes["bugsnag.system.cpu_mean_overhead"] =
112-
overheadCpuTotal / overheadSampleCount
113+
(overheadCpuTotal / overheadSampleCount).ensurePositive()
113114
}
114115

115116
snapshot.blocking?.cancel()
@@ -125,6 +126,10 @@ internal class CpuMetricsSource(
125126
return "cpuMetrics"
126127
}
127128

129+
private fun Double.ensurePositive(): Double {
130+
return if (isFinite() && this > 0.0) this else 0.0
131+
}
132+
128133
private class CpuSampleData(
129134
@JvmField
130135
var processCpuPct: Double = 0.0,
@@ -181,7 +186,8 @@ private class CpuMetricsSampler(statFile: String) {
181186
previousUptime = uptimeSec
182187

183188
val cpuUsagePercent = 100.0 * (deltaCpuTime / deltaUptime)
184-
return cpuUsagePercent / SystemConfig.numCores
189+
val normalisedPct = cpuUsagePercent / SystemConfig.numCores
190+
return if (normalisedPct.isFinite()) normalisedPct else 0.0
185191
}
186192
}
187193

@@ -219,10 +225,15 @@ internal object SystemConfig {
219225
*/
220226
val clockTickHz: Double get() = _clockTickHz
221227

222-
val numCores: Int =
223-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
228+
val numCores: Int = numProcessorCores()
229+
230+
private fun numProcessorCores(): Int {
231+
val cores = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
224232
Os.sysconf(OsConstants._SC_NPROCESSORS_CONF).toInt()
225233
} else {
226234
Runtime.getRuntime().availableProcessors()
227235
}
236+
237+
return max(cores, 1)
238+
}
228239
}

bugsnag-android-performance/src/main/kotlin/com/bugsnag/android/performance/internal/processing/JsonTraceWriter.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,21 @@ internal class JsonTraceWriter(
6565
}
6666

6767
fun value(value: Double): JsonTraceWriter {
68+
if (!value.isFinite()) {
69+
json.nullValue()
70+
return this
71+
}
72+
6873
json.value(value)
6974
return this
7075
}
7176

7277
fun value(value: Number): JsonTraceWriter {
78+
if (value is Double || value is Float) {
79+
value(value.toDouble())
80+
return this
81+
}
82+
7383
json.value(value)
7484
return this
7585
}

0 commit comments

Comments
 (0)