Skip to content

Support setting error type on individual stack frames #1001

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

### Enhancements

* Support native stack traces in the ANR plugin
* Support setting error type on individual stack frames
[#1001](https://github.com/bugsnag/bugsnag-android/pull/1001)

* Support native stack traces in the ANR plugin
[#972](https://github.com/bugsnag/bugsnag-android/pull/972)

## 5.2.3 (2020-11-04)
Expand Down
1 change: 1 addition & 0 deletions bugsnag-android-core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<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>
<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>
<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>
<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>
<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>
<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>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$299</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void setUp() {
Event event = new Event(exception, config, handledState, NoopLogger.INSTANCE);
event.setApp(generateAppWithState());
event.setDevice(generateDeviceWithState());
eventPayload = new EventPayload("api-key", event, new Notifier());
eventPayload = new EventPayload("api-key", event, new Notifier(), config);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ void populateAndNotifyAndroidEvent(@NonNull Event event,

void notifyInternal(@NonNull Event event,
@Nullable OnErrorCallback onError) {
String type = event.impl.getSeverityReasonType();
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()) {
Expand All @@ -680,7 +680,7 @@ void notifyInternal(@NonNull Event event,
// set the redacted keys on the event as this
// will not have been set for RN/Unity events
Set<String> redactedKeys = metadataState.getMetadata().getRedactedKeys();
Metadata eventMetadata = event.impl.getMetadata();
Metadata eventMetadata = event.getImpl().getMetadata();
eventMetadata.setRedactedKeys(redactedKeys);

// get session for event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
Expand All @@ -32,7 +33,8 @@ class DeliveryDelegate extends BaseObservable {
void deliver(@NonNull Event event) {
logger.d("DeliveryDelegate#deliver() - event being stored/delivered by Client");
// Build the eventPayload
EventPayload eventPayload = new EventPayload(event.getApiKey(), event, notifier);
String apiKey = event.getApiKey();
EventPayload eventPayload = new EventPayload(apiKey, event, notifier, immutableConfig);
Session session = event.getSession();

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

if (event.isUnhandled()) {
// should only send unhandled errors if they don't terminate the process (i.e. ANRs)
String severityReasonType = event.impl.getSeverityReasonType();
String severityReasonType = event.getImpl().getSeverityReasonType();
boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType);
boolean anr = event.impl.isAnr(event);
boolean anr = event.getImpl().isAnr(event);
cacheEvent(event, anr || promiseRejection);
} else {
deliverPayloadAsync(event, eventPayload);
Expand Down Expand Up @@ -77,9 +79,7 @@ public void run() {
@VisibleForTesting
DeliveryStatus deliverPayloadInternal(@NonNull EventPayload payload, @NonNull Event event) {
logger.d("DeliveryDelegate#deliverPayloadInternal() - attempting event delivery");

String apiKey = payload.getApiKey();
DeliveryParams deliveryParams = immutableConfig.getErrorApiDeliveryParams(apiKey);
DeliveryParams deliveryParams = immutableConfig.getErrorApiDeliveryParams(payload);
Delivery delivery = immutableConfig.getDelivery();
DeliveryStatus deliveryStatus = delivery.deliver(payload, deliveryParams);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.util.Date

private const val HEADER_API_PAYLOAD_VERSION = "Bugsnag-Payload-Version"
private const val HEADER_BUGSNAG_SENT_AT = "Bugsnag-Sent-At"
private const val HEADER_BUGSNAG_STACKTRACE_TYPES = "Bugsnag-Stacktrace-Types"
internal const val HEADER_API_KEY = "Bugsnag-Api-Key"
internal const val HEADER_INTERNAL_ERROR = "Bugsnag-Internal-Error"

Expand All @@ -12,11 +13,32 @@ internal const val HEADER_INTERNAL_ERROR = "Bugsnag-Internal-Error"
*
* @return the HTTP headers
*/
internal fun errorApiHeaders(apiKey: String) = mapOf(
HEADER_API_PAYLOAD_VERSION to "4.0",
HEADER_API_KEY to apiKey,
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date())
)
internal fun errorApiHeaders(payload: EventPayload): Map<String, String> {
val mutableHeaders = mutableMapOf(
HEADER_API_PAYLOAD_VERSION to "4.0",
HEADER_API_KEY to payload.apiKey!!,
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date())
)
val errorTypes = payload.getErrorTypes()
if (errorTypes.isNotEmpty()) {
mutableHeaders[HEADER_BUGSNAG_STACKTRACE_TYPES] = serializeErrorTypeHeader(errorTypes)
}
return mutableHeaders.toMap()
}

/**
* Serializes the error types to a comma delimited string
*/
internal fun serializeErrorTypeHeader(errorTypes: Set<ErrorType>): String {
return when {
errorTypes.isEmpty() -> ""
else -> errorTypes
.map(ErrorType::desc)
.reduce { accumulator, str ->
"$accumulator,$str"
}
}
}

/**
* Supplies the headers which must be used in any request sent to the Session Tracking API.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@SuppressWarnings("ConstantConditions")
public class Event implements JsonStream.Streamable, MetadataAware, UserAware {

final EventInternal impl;
private final EventInternal impl;
private final Logger logger;

Event(@Nullable Throwable originalError,
Expand Down Expand Up @@ -322,4 +322,8 @@ Session getSession() {
void setSession(@Nullable Session session) {
impl.session = session;
}

EventInternal getImpl() {
return impl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.bugsnag.android

import java.io.File
import java.util.Locale
import java.util.UUID

/**
* Represents important information about an event which is encoded/decoded from a filename.
* Currently the following information is encoded:
*
* apiKey - as a user can decide to override the value on an Event
* uuid - to disambiguate stored error reports
* timestamp - to sort error reports by time of capture
* suffix - used to encode whether the app crashed on launch, or the report is not a JVM error
* errorTypes - a comma delimited string which contains the stackframe types in the error
*/
internal data class EventFilenameInfo(
val apiKey: String,
val uuid: String,
val timestamp: Long,
val suffix: String,
val errorTypes: Set<ErrorType>
) {

/**
* Generates a filename for the Event in the format
* "[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
)
}

fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH

internal companion object {
private const val STARTUP_CRASH = "startupcrash"
private const val NON_JVM_CRASH = "not-jvm"

@JvmOverloads
fun fromEvent(
obj: Any,
uuid: String = UUID.randomUUID().toString(),
apiKey: String?,
timestamp: Long = System.currentTimeMillis(),
config: ImmutableConfig
): EventFilenameInfo {
val sanitizedApiKey = when {
obj is Event -> obj.apiKey
apiKey.isNullOrEmpty() -> config.apiKey
else -> apiKey
}

return EventFilenameInfo(
sanitizedApiKey,
uuid,
timestamp,
findSuffixForEvent(obj, config),
findErrorTypesForEvent(obj)
)
}

/**
* Reads event information from a filename.
*/
fun fromFile(file: File, config: ImmutableConfig): EventFilenameInfo {
return EventFilenameInfo(
findApiKeyInFilename(file, config),
"", // ignore UUID field when reading from file as unused
-1, // ignore timestamp when reading from file as unused
findSuffixInFilename(file),
findErrorTypesInFilename(file)
)
}

/**
* Retrieves the api key encoded in the filename, or an empty string if this information
* is not encoded for the given event
*/
private fun findApiKeyInFilename(file: File, config: ImmutableConfig): String {
val name = file.name.replace("_$STARTUP_CRASH.json".toRegex(), "")
val start = name.indexOf("_") + 1
val end = name.indexOf("_", start)
val apiKey = if (start == 0 || end == -1 || end <= start) {
null
} else {
name.substring(start, end)
}
return apiKey ?: config.apiKey
}

/**
* Retrieves the error types encoded in the filename, or an empty string if this
* information is not encoded for the given event
*/
private fun findErrorTypesInFilename(eventFile: File): Set<ErrorType> {
val name = eventFile.name
val end = name.lastIndexOf("_", name.lastIndexOf("_") - 1)
val start = name.lastIndexOf("_", end - 1) + 1

if (start < end) {
val encodedValues: List<String> = name.substring(start, end).split(",")
return ErrorType.values().filter {
encodedValues.contains(it.desc)
}.toSet()
}
return emptySet()
}

/**
* Retrieves the error types encoded in the filename, or an empty string if this
* information is not encoded for the given event
*/
private fun findSuffixInFilename(eventFile: File): String {
val name = eventFile.nameWithoutExtension
val suffix = name.substring(name.lastIndexOf("_") + 1)
return when (suffix) {
STARTUP_CRASH, NON_JVM_CRASH -> suffix
else -> ""
}
}

/**
* Retrieves the error types for the given event
*/
private fun findErrorTypesForEvent(obj: Any): Set<ErrorType> {
return when (obj) {
is Event -> obj.impl.getErrorTypesFromStackframes()
else -> setOf(ErrorType.C)
}
}

/**
* Calculates the suffix for the given event
*/
private fun findSuffixForEvent(obj: Any, config: ImmutableConfig): String {
return when (obj) {
is Event -> {
val duration = obj.app.duration
if (duration != null && isStartupCrash(duration.toLong(), config)) {
STARTUP_CRASH
} else {
""
}
}
else -> {
NON_JVM_CRASH
}
}
}

private fun isStartupCrash(durationMs: Long, config: ImmutableConfig): Boolean {
return durationMs < config.launchCrashThresholdMs
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ internal class EventInternal @JvmOverloads internal constructor(
writer.endObject()
}

internal fun getErrorTypesFromStackframes(): Set<ErrorType> {
val errorTypes = errors.mapNotNull(Error::getType).toSet()
val frameOverrideTypes = errors
.map { it.stacktrace }
.flatMap { it.mapNotNull(Stackframe::type) }
return errorTypes.plus(frameOverrideTypes)
}

protected fun updateSeverityInternal(severity: Severity) {
handledState = HandledState.newInstance(handledState.severityReasonType,
severity, handledState.attributeValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,40 @@ class EventPayload : JsonStream.Streamable {
private val eventFile: File?
val event: Event?
private val notifier: Notifier
private val config: ImmutableConfig

internal constructor(apiKey: String?, eventFile: File, notifier: Notifier) {
internal constructor(
apiKey: String?,
eventFile: File,
notifier: Notifier,
config: ImmutableConfig
) {
this.apiKey = apiKey
this.eventFile = eventFile
this.event = null
this.notifier = notifier
this.config = config
}

internal constructor(apiKey: String?, event: Event, notifier: Notifier) {
internal constructor(
apiKey: String?,
event: Event,
notifier: Notifier,
config: ImmutableConfig
) {
this.apiKey = apiKey
this.eventFile = null
this.event = event
this.notifier = notifier
this.config = config
}

internal fun getErrorTypes(): Set<ErrorType> {
return when {
event != null -> event.impl.getErrorTypesFromStackframes()
eventFile != null -> EventFilenameInfo.fromFile(eventFile, config).errorTypes
else -> emptySet()
}
}

@Throws(IOException::class)
Expand All @@ -36,7 +57,6 @@ class EventPayload : JsonStream.Streamable {
writer.name("apiKey").value(apiKey)
writer.name("payloadVersion").value("4.0")
writer.name("notifier").value(notifier)

writer.name("events").beginArray()

when {
Expand Down
Loading