Skip to content

Commit 5461777

Browse files
Merge pull request #1001 from bugsnag/PLAT-5420/stacktraces
Support setting error type on individual stack frames
2 parents a22f246 + b601bbd commit 5461777

40 files changed

+1038
-401
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
### Enhancements
66

7-
* Support native stack traces in the ANR plugin
7+
* Support setting error type on individual stack frames
8+
[#1001](https://github.com/bugsnag/bugsnag-android/pull/1001)
9+
10+
* Support native stack traces in the ANR plugin
811
[#972](https://github.com/bugsnag/bugsnag-android/pull/972)
912

1013
## 5.2.3 (2020-11-04)

bugsnag-android-core/detekt-baseline.xml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<ID>LongParameterList:DeviceBuildInfo.kt$DeviceBuildInfo$( val manufacturer: String?, val model: String?, val osVersion: String?, val apiLevel: Int?, val osBuild: String?, val fingerprint: String?, val tags: String?, val brand: String?, val cpuAbis: Array&lt;String&gt;? )</ID>
1212
<ID>LongParameterList:DeviceDataCollector.kt$DeviceDataCollector$( private val connectivity: Connectivity, private val appContext: Context, private val resources: Resources?, private val installId: String, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, private val logger: Logger )</ID>
1313
<ID>LongParameterList:DeviceWithState.kt$DeviceWithState$( buildInfo: DeviceBuildInfo, jailbroken: Boolean?, id: String?, locale: String?, totalMemory: Long?, runtimeVersions: MutableMap&lt;String, Any&gt;, /** * The number of free bytes of storage available on the device */ var freeDisk: Long?, /** * The number of free bytes of memory available on the device */ var freeMemory: Long?, /** * The orientation of the device when the event occurred: either portrait or landscape */ var orientation: String?, /** * The timestamp on the device when the event occurred */ var time: Date? )</ID>
14+
<ID>LongParameterList:NativeStackframe.kt$NativeStackframe$( /** * The name of the method that was being executed */ var method: String?, /** * The location of the source file */ var file: String?, /** * The line number within the source file this stackframe refers to */ var lineNumber: Number?, /** * The address of the instruction where the event occurred. */ var frameAddress: Long?, /** * The address of the function where the event occurred. */ var symbolAddress: Long?, /** * The address of the library where the event occurred. */ var loadAddress: Long?, /** * The type of the error */ var type: ErrorType = ErrorType.C )</ID>
1415
<ID>LongParameterList:ThreadState.kt$ThreadState$( exc: Throwable?, isUnhandled: Boolean, sendThreads: ThreadSendPolicy, projectPackages: Collection&lt;String&gt;, logger: Logger, currentThread: java.lang.Thread = java.lang.Thread.currentThread(), stackTraces: MutableMap&lt;java.lang.Thread, Array&lt;StackTraceElement&gt;&gt; = java.lang.Thread.getAllStackTraces() )</ID>
1516
<ID>LongParameterList:ThreadState.kt$ThreadState$( stackTraces: MutableMap&lt;java.lang.Thread, Array&lt;StackTraceElement&gt;&gt;, currentThread: java.lang.Thread, exc: Throwable?, isUnhandled: Boolean, projectPackages: Collection&lt;String&gt;, logger: Logger )</ID>
1617
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$299</ID>

bugsnag-android-core/src/androidTest/java/com/bugsnag/android/EventPayloadTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public void setUp() {
3232
Event event = new Event(exception, config, handledState, NoopLogger.INSTANCE);
3333
event.setApp(generateAppWithState());
3434
event.setDevice(generateDeviceWithState());
35-
eventPayload = new EventPayload("api-key", event, new Notifier());
35+
eventPayload = new EventPayload("api-key", event, new Notifier(), config);
3636
}
3737

3838
@Test

bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ void populateAndNotifyAndroidEvent(@NonNull Event event,
666666

667667
void notifyInternal(@NonNull Event event,
668668
@Nullable OnErrorCallback onError) {
669-
String type = event.impl.getSeverityReasonType();
669+
String type = event.getImpl().getSeverityReasonType();
670670
logger.d("Client#notifyInternal() - event captured by Client, type=" + type);
671671
// Don't notify if this event class should be ignored
672672
if (event.shouldDiscardClass()) {
@@ -680,7 +680,7 @@ void notifyInternal(@NonNull Event event,
680680
// set the redacted keys on the event as this
681681
// will not have been set for RN/Unity events
682682
Set<String> redactedKeys = metadataState.getMetadata().getRedactedKeys();
683-
Metadata eventMetadata = event.impl.getMetadata();
683+
Metadata eventMetadata = event.getImpl().getMetadata();
684684
eventMetadata.setRedactedKeys(redactedKeys);
685685

686686
// get session for event

bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryDelegate.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import java.util.Date;
99
import java.util.HashMap;
10+
import java.util.HashSet;
1011
import java.util.List;
1112
import java.util.Map;
1213
import java.util.concurrent.RejectedExecutionException;
@@ -32,7 +33,8 @@ class DeliveryDelegate extends BaseObservable {
3233
void deliver(@NonNull Event event) {
3334
logger.d("DeliveryDelegate#deliver() - event being stored/delivered by Client");
3435
// Build the eventPayload
35-
EventPayload eventPayload = new EventPayload(event.getApiKey(), event, notifier);
36+
String apiKey = event.getApiKey();
37+
EventPayload eventPayload = new EventPayload(apiKey, event, notifier, immutableConfig);
3638
Session session = event.getSession();
3739

3840
if (session != null) {
@@ -47,9 +49,9 @@ void deliver(@NonNull Event event) {
4749

4850
if (event.isUnhandled()) {
4951
// should only send unhandled errors if they don't terminate the process (i.e. ANRs)
50-
String severityReasonType = event.impl.getSeverityReasonType();
52+
String severityReasonType = event.getImpl().getSeverityReasonType();
5153
boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType);
52-
boolean anr = event.impl.isAnr(event);
54+
boolean anr = event.getImpl().isAnr(event);
5355
cacheEvent(event, anr || promiseRejection);
5456
} else {
5557
deliverPayloadAsync(event, eventPayload);
@@ -77,9 +79,7 @@ public void run() {
7779
@VisibleForTesting
7880
DeliveryStatus deliverPayloadInternal(@NonNull EventPayload payload, @NonNull Event event) {
7981
logger.d("DeliveryDelegate#deliverPayloadInternal() - attempting event delivery");
80-
81-
String apiKey = payload.getApiKey();
82-
DeliveryParams deliveryParams = immutableConfig.getErrorApiDeliveryParams(apiKey);
82+
DeliveryParams deliveryParams = immutableConfig.getErrorApiDeliveryParams(payload);
8383
Delivery delivery = immutableConfig.getDelivery();
8484
DeliveryStatus deliveryStatus = delivery.deliver(payload, deliveryParams);
8585

bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryHeaders.kt

+27-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import java.util.Date
44

55
private const val HEADER_API_PAYLOAD_VERSION = "Bugsnag-Payload-Version"
66
private const val HEADER_BUGSNAG_SENT_AT = "Bugsnag-Sent-At"
7+
private const val HEADER_BUGSNAG_STACKTRACE_TYPES = "Bugsnag-Stacktrace-Types"
78
internal const val HEADER_API_KEY = "Bugsnag-Api-Key"
89
internal const val HEADER_INTERNAL_ERROR = "Bugsnag-Internal-Error"
910

@@ -12,11 +13,32 @@ internal const val HEADER_INTERNAL_ERROR = "Bugsnag-Internal-Error"
1213
*
1314
* @return the HTTP headers
1415
*/
15-
internal fun errorApiHeaders(apiKey: String) = mapOf(
16-
HEADER_API_PAYLOAD_VERSION to "4.0",
17-
HEADER_API_KEY to apiKey,
18-
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date())
19-
)
16+
internal fun errorApiHeaders(payload: EventPayload): Map<String, String> {
17+
val mutableHeaders = mutableMapOf(
18+
HEADER_API_PAYLOAD_VERSION to "4.0",
19+
HEADER_API_KEY to payload.apiKey!!,
20+
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date())
21+
)
22+
val errorTypes = payload.getErrorTypes()
23+
if (errorTypes.isNotEmpty()) {
24+
mutableHeaders[HEADER_BUGSNAG_STACKTRACE_TYPES] = serializeErrorTypeHeader(errorTypes)
25+
}
26+
return mutableHeaders.toMap()
27+
}
28+
29+
/**
30+
* Serializes the error types to a comma delimited string
31+
*/
32+
internal fun serializeErrorTypeHeader(errorTypes: Set<ErrorType>): String {
33+
return when {
34+
errorTypes.isEmpty() -> ""
35+
else -> errorTypes
36+
.map(ErrorType::desc)
37+
.reduce { accumulator, str ->
38+
"$accumulator,$str"
39+
}
40+
}
41+
}
2042

2143
/**
2244
* Supplies the headers which must be used in any request sent to the Session Tracking API.

bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
@SuppressWarnings("ConstantConditions")
1616
public class Event implements JsonStream.Streamable, MetadataAware, UserAware {
1717

18-
final EventInternal impl;
18+
private final EventInternal impl;
1919
private final Logger logger;
2020

2121
Event(@Nullable Throwable originalError,
@@ -322,4 +322,8 @@ Session getSession() {
322322
void setSession(@Nullable Session session) {
323323
impl.session = session;
324324
}
325+
326+
EventInternal getImpl() {
327+
return impl;
328+
}
325329
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package com.bugsnag.android
2+
3+
import java.io.File
4+
import java.util.Locale
5+
import java.util.UUID
6+
7+
/**
8+
* Represents important information about an event which is encoded/decoded from a filename.
9+
* Currently the following information is encoded:
10+
*
11+
* apiKey - as a user can decide to override the value on an Event
12+
* uuid - to disambiguate stored error reports
13+
* timestamp - to sort error reports by time of capture
14+
* suffix - used to encode whether the app crashed on launch, or the report is not a JVM error
15+
* errorTypes - a comma delimited string which contains the stackframe types in the error
16+
*/
17+
internal data class EventFilenameInfo(
18+
val apiKey: String,
19+
val uuid: String,
20+
val timestamp: Long,
21+
val suffix: String,
22+
val errorTypes: Set<ErrorType>
23+
) {
24+
25+
/**
26+
* Generates a filename for the Event in the format
27+
* "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json"
28+
*/
29+
fun encode(): String {
30+
return String.format(
31+
Locale.US,
32+
"%d_%s_%s_%s_%s.json",
33+
timestamp,
34+
apiKey,
35+
serializeErrorTypeHeader(errorTypes),
36+
uuid,
37+
suffix
38+
)
39+
}
40+
41+
fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH
42+
43+
internal companion object {
44+
private const val STARTUP_CRASH = "startupcrash"
45+
private const val NON_JVM_CRASH = "not-jvm"
46+
47+
@JvmOverloads
48+
fun fromEvent(
49+
obj: Any,
50+
uuid: String = UUID.randomUUID().toString(),
51+
apiKey: String?,
52+
timestamp: Long = System.currentTimeMillis(),
53+
config: ImmutableConfig
54+
): EventFilenameInfo {
55+
val sanitizedApiKey = when {
56+
obj is Event -> obj.apiKey
57+
apiKey.isNullOrEmpty() -> config.apiKey
58+
else -> apiKey
59+
}
60+
61+
return EventFilenameInfo(
62+
sanitizedApiKey,
63+
uuid,
64+
timestamp,
65+
findSuffixForEvent(obj, config),
66+
findErrorTypesForEvent(obj)
67+
)
68+
}
69+
70+
/**
71+
* Reads event information from a filename.
72+
*/
73+
fun fromFile(file: File, config: ImmutableConfig): EventFilenameInfo {
74+
return EventFilenameInfo(
75+
findApiKeyInFilename(file, config),
76+
"", // ignore UUID field when reading from file as unused
77+
-1, // ignore timestamp when reading from file as unused
78+
findSuffixInFilename(file),
79+
findErrorTypesInFilename(file)
80+
)
81+
}
82+
83+
/**
84+
* Retrieves the api key encoded in the filename, or an empty string if this information
85+
* is not encoded for the given event
86+
*/
87+
private fun findApiKeyInFilename(file: File, config: ImmutableConfig): String {
88+
val name = file.name.replace("_$STARTUP_CRASH.json".toRegex(), "")
89+
val start = name.indexOf("_") + 1
90+
val end = name.indexOf("_", start)
91+
val apiKey = if (start == 0 || end == -1 || end <= start) {
92+
null
93+
} else {
94+
name.substring(start, end)
95+
}
96+
return apiKey ?: config.apiKey
97+
}
98+
99+
/**
100+
* Retrieves the error types encoded in the filename, or an empty string if this
101+
* information is not encoded for the given event
102+
*/
103+
private fun findErrorTypesInFilename(eventFile: File): Set<ErrorType> {
104+
val name = eventFile.name
105+
val end = name.lastIndexOf("_", name.lastIndexOf("_") - 1)
106+
val start = name.lastIndexOf("_", end - 1) + 1
107+
108+
if (start < end) {
109+
val encodedValues: List<String> = name.substring(start, end).split(",")
110+
return ErrorType.values().filter {
111+
encodedValues.contains(it.desc)
112+
}.toSet()
113+
}
114+
return emptySet()
115+
}
116+
117+
/**
118+
* Retrieves the error types encoded in the filename, or an empty string if this
119+
* information is not encoded for the given event
120+
*/
121+
private fun findSuffixInFilename(eventFile: File): String {
122+
val name = eventFile.nameWithoutExtension
123+
val suffix = name.substring(name.lastIndexOf("_") + 1)
124+
return when (suffix) {
125+
STARTUP_CRASH, NON_JVM_CRASH -> suffix
126+
else -> ""
127+
}
128+
}
129+
130+
/**
131+
* Retrieves the error types for the given event
132+
*/
133+
private fun findErrorTypesForEvent(obj: Any): Set<ErrorType> {
134+
return when (obj) {
135+
is Event -> obj.impl.getErrorTypesFromStackframes()
136+
else -> setOf(ErrorType.C)
137+
}
138+
}
139+
140+
/**
141+
* Calculates the suffix for the given event
142+
*/
143+
private fun findSuffixForEvent(obj: Any, config: ImmutableConfig): String {
144+
return when (obj) {
145+
is Event -> {
146+
val duration = obj.app.duration
147+
if (duration != null && isStartupCrash(duration.toLong(), config)) {
148+
STARTUP_CRASH
149+
} else {
150+
""
151+
}
152+
}
153+
else -> {
154+
NON_JVM_CRASH
155+
}
156+
}
157+
}
158+
159+
private fun isStartupCrash(durationMs: Long, config: ImmutableConfig): Boolean {
160+
return durationMs < config.launchCrashThresholdMs
161+
}
162+
}
163+
}
164+

bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt

+8
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ internal class EventInternal @JvmOverloads internal constructor(
104104
writer.endObject()
105105
}
106106

107+
internal fun getErrorTypesFromStackframes(): Set<ErrorType> {
108+
val errorTypes = errors.mapNotNull(Error::getType).toSet()
109+
val frameOverrideTypes = errors
110+
.map { it.stacktrace }
111+
.flatMap { it.mapNotNull(Stackframe::type) }
112+
return errorTypes.plus(frameOverrideTypes)
113+
}
114+
107115
protected fun updateSeverityInternal(severity: Severity) {
108116
handledState = HandledState.newInstance(handledState.severityReasonType,
109117
severity, handledState.attributeValue)

bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt

+23-3
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,40 @@ class EventPayload : JsonStream.Streamable {
1515
private val eventFile: File?
1616
val event: Event?
1717
private val notifier: Notifier
18+
private val config: ImmutableConfig
1819

19-
internal constructor(apiKey: String?, eventFile: File, notifier: Notifier) {
20+
internal constructor(
21+
apiKey: String?,
22+
eventFile: File,
23+
notifier: Notifier,
24+
config: ImmutableConfig
25+
) {
2026
this.apiKey = apiKey
2127
this.eventFile = eventFile
2228
this.event = null
2329
this.notifier = notifier
30+
this.config = config
2431
}
2532

26-
internal constructor(apiKey: String?, event: Event, notifier: Notifier) {
33+
internal constructor(
34+
apiKey: String?,
35+
event: Event,
36+
notifier: Notifier,
37+
config: ImmutableConfig
38+
) {
2739
this.apiKey = apiKey
2840
this.eventFile = null
2941
this.event = event
3042
this.notifier = notifier
43+
this.config = config
44+
}
45+
46+
internal fun getErrorTypes(): Set<ErrorType> {
47+
return when {
48+
event != null -> event.impl.getErrorTypesFromStackframes()
49+
eventFile != null -> EventFilenameInfo.fromFile(eventFile, config).errorTypes
50+
else -> emptySet()
51+
}
3152
}
3253

3354
@Throws(IOException::class)
@@ -36,7 +57,6 @@ class EventPayload : JsonStream.Streamable {
3657
writer.name("apiKey").value(apiKey)
3758
writer.name("payloadVersion").value("4.0")
3859
writer.name("notifier").value(notifier)
39-
4060
writer.name("events").beginArray()
4161

4262
when {

0 commit comments

Comments
 (0)