Updated Bugsnag

master
M66B 1 month ago
parent 113898de3b
commit 552d4403c2

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

@ -2,6 +2,8 @@ package com.bugsnag.android
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import java.util.WeakHashMap import java.util.WeakHashMap
@ -11,8 +13,16 @@ internal class ActivityBreadcrumbCollector(
private val prevState = WeakHashMap<Activity, String>() private val prevState = WeakHashMap<Activity, String>()
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
leaveBreadcrumb(activity, "onCreate()", savedInstanceState != null) leaveBreadcrumb(
activity,
"onCreate()",
mutableMapOf<String, Any>().apply {
set("hasBundle", savedInstanceState != null)
setActivityIntentMetadata(activity.intent)
}
)
}
override fun onActivityStarted(activity: Activity) = override fun onActivityStarted(activity: Activity) =
leaveBreadcrumb(activity, "onStart()") leaveBreadcrumb(activity, "onStart()")
@ -39,12 +49,8 @@ internal class ActivityBreadcrumbCollector(
private fun leaveBreadcrumb( private fun leaveBreadcrumb(
activity: Activity, activity: Activity,
lifecycleCallback: String, lifecycleCallback: String,
hasBundle: Boolean? = null metadata: MutableMap<String, Any> = mutableMapOf()
) { ) {
val metadata = mutableMapOf<String, Any>()
if (hasBundle != null) {
metadata["hasBundle"] = hasBundle
}
val previousVal = prevState[activity] val previousVal = prevState[activity]
if (previousVal != null) { if (previousVal != null) {
@ -55,4 +61,24 @@ internal class ActivityBreadcrumbCollector(
cb("$activityName#$lifecycleCallback", metadata) cb("$activityName#$lifecycleCallback", metadata)
prevState[activity] = lifecycleCallback prevState[activity] = lifecycleCallback
} }
private fun MutableMap<String, Any>.setActivityIntentMetadata(intent: Intent?) {
if (intent == null) return
intent.action?.let { set("action", it) }
intent.categories?.let { set("categories", it.joinToString(", ")) }
intent.type?.let { set("type", it) }
if (intent.flags != 0) {
@Suppress("MagicNumber") // hex radix
set("flags", "0x${intent.flags.toString(16)}")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
intent.identifier?.let { set("id", it) }
}
set("hasData", intent.data != null)
set("hasExtras", intent.extras?.keySet()?.joinToString(", ") ?: false)
}
} }

@ -1,5 +1,6 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.DateUtils
import java.io.IOException import java.io.IOException
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -42,8 +43,7 @@ internal class BreadcrumbState(
StateEvent.AddBreadcrumb( StateEvent.AddBreadcrumb(
breadcrumb.impl.message, breadcrumb.impl.message,
breadcrumb.impl.type, breadcrumb.impl.type,
// an encoding of milliseconds since the epoch DateUtils.toIso8601(breadcrumb.impl.timestamp),
"t${breadcrumb.impl.timestamp.time}",
breadcrumb.impl.metadata ?: mutableMapOf() breadcrumb.impl.metadata ?: mutableMapOf()
) )
} }

@ -7,6 +7,7 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import java.util.UUID
internal class BugsnagEventMapper( internal class BugsnagEventMapper(
private val logger: Logger private val logger: Logger
@ -93,6 +94,16 @@ internal class BugsnagEventMapper(
// populate internalMetrics // populate internalMetrics
event.internalMetrics = InternalMetricsImpl(map["usage"] as MutableMap<String, Any>?) event.internalMetrics = InternalMetricsImpl(map["usage"] as MutableMap<String, Any>?)
// populate correlation
(map["correlation"] as? Map<String, String>)?.let {
val traceId = parseTraceId(it["traceId"])
val spanId = it["spanId"]?.parseUnsignedLong()
if (traceId != null && spanId != null) {
event.traceCorrelation = TraceCorrelation(traceId, spanId)
}
}
return event return event
} }
@ -184,14 +195,14 @@ internal class BugsnagEventMapper(
thread.readEntry("name"), thread.readEntry("name"),
ErrorType.fromDescriptor(thread.readEntry("type")) ?: ErrorType.ANDROID, ErrorType.fromDescriptor(thread.readEntry("type")) ?: ErrorType.ANDROID,
thread["errorReportingThread"] == true, thread["errorReportingThread"] == true,
thread.readEntry("state"), thread["state"] as? String ?: "",
(thread["stacktrace"] as? List<Map<String, Any?>>)?.let { convertStacktrace(it) } (thread["stacktrace"] as? List<Map<String, Any?>>)?.let { convertStacktrace(it) }
?: Stacktrace(emptyList()) ?: Stacktrace(mutableListOf())
) )
} }
internal fun convertStacktrace(trace: List<Map<String, Any?>>): Stacktrace { internal fun convertStacktrace(trace: List<Map<String, Any?>>): Stacktrace {
return Stacktrace(trace.map { Stackframe(it) }) return Stacktrace(trace.mapTo(ArrayList(trace.size)) { Stackframe(it) })
} }
internal fun deserializeSeverityReason( internal fun deserializeSeverityReason(
@ -234,16 +245,15 @@ internal class BugsnagEventMapper(
} }
} }
// SimpleDateFormat isn't thread safe, cache one instance per thread as needed. private fun String.toDate(): Date {
private val ndkDateFormatHolder = object : ThreadLocal<DateFormat>() { if (isNotEmpty() && this[0] == 't') {
override fun initialValue(): DateFormat { // date is in the format 't{epoch millis}'
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { val timestamp = substring(1)
timeZone = TimeZone.getTimeZone("UTC") timestamp.toLongOrNull()?.let {
return Date(it)
} }
} }
}
private fun String.toDate(): Date {
return try { return try {
DateUtils.fromIso8601(this) DateUtils.fromIso8601(this)
} catch (pe: IllegalArgumentException) { } catch (pe: IllegalArgumentException) {
@ -251,4 +261,31 @@ internal class BugsnagEventMapper(
?: throw IllegalArgumentException("cannot parse date $this") ?: throw IllegalArgumentException("cannot parse date $this")
} }
} }
private fun parseTraceId(traceId: String?): UUID? {
if (traceId?.length != 32) return null
val mostSigBits = traceId.substring(0, 16).parseUnsignedLong() ?: return null
val leastSigBits = traceId.substring(16).parseUnsignedLong() ?: return null
return UUID(mostSigBits, leastSigBits)
}
private fun String.parseUnsignedLong(): Long? {
if (length != 16) return null
return try {
(substring(0, 2).toLong(16) shl 56) or
substring(2).toLong(16)
} catch (nfe: NumberFormatException) {
null
}
}
// SimpleDateFormat isn't thread safe, cache one instance per thread as needed.
private val ndkDateFormatHolder = object : ThreadLocal<DateFormat>() {
override fun initialValue(): DateFormat {
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
}
} }

@ -22,7 +22,8 @@ internal class ClientObservable : BaseObservable() {
conf.releaseStage, conf.releaseStage,
lastRunInfoPath, lastRunInfoPath,
consecutiveLaunchCrashes, consecutiveLaunchCrashes,
conf.sendThreads conf.sendThreads,
conf.maxBreadcrumbs
) )
} }
} }

@ -88,6 +88,16 @@ public class Error implements JsonStream.Streamable {
return impl.getStacktrace(); return impl.getStacktrace();
} }
/**
* Add a new stackframe to the end of this Error returning the new Stackframe data object.
*/
@NonNull
public Stackframe addStackframe(@Nullable String method,
@Nullable String file,
long lineNumber) {
return impl.addStackframe(method, file, lineNumber);
}
@Override @Override
public void toStream(@NonNull JsonStream stream) throws IOException { public void toStream(@NonNull JsonStream stream) throws IOException {
impl.toStream(stream); impl.toStream(stream);
@ -98,4 +108,4 @@ public class Error implements JsonStream.Streamable {
@NonNull Logger logger) { @NonNull Logger logger) {
return ErrorInternal.Companion.createError(exc, projectPackages, logger); return ErrorInternal.Companion.createError(exc, projectPackages, logger);
} }
} }

@ -7,17 +7,30 @@ internal class ErrorInternal @JvmOverloads internal constructor(
var type: ErrorType = ErrorType.ANDROID var type: ErrorType = ErrorType.ANDROID
) : JsonStream.Streamable { ) : JsonStream.Streamable {
val stacktrace: List<Stackframe> = stacktrace.trace val stacktrace: MutableList<Stackframe> = stacktrace.trace
fun addStackframe(method: String?, file: String?, lineNumber: Long): Stackframe {
val frame = Stackframe(method, file, lineNumber, null)
stacktrace.add(frame)
return frame
}
internal companion object { internal companion object {
fun createError(exc: Throwable, projectPackages: Collection<String>, logger: Logger): MutableList<Error> { fun createError(
exc: Throwable,
projectPackages: Collection<String>,
logger: Logger
): MutableList<Error> {
return exc.safeUnrollCauses() return exc.safeUnrollCauses()
.mapTo(mutableListOf()) { currentEx -> .mapTo(mutableListOf()) { currentEx ->
// Somehow it's possible for stackTrace to be null in rare cases // Somehow it's possible for stackTrace to be null in rare cases
val stacktrace = currentEx.stackTrace ?: arrayOf<StackTraceElement>() val stacktrace = currentEx.stackTrace ?: arrayOf<StackTraceElement>()
val trace = Stacktrace(stacktrace, projectPackages, logger) val trace = Stacktrace(stacktrace, projectPackages, logger)
val errorInternal = val errorInternal = ErrorInternal(
ErrorInternal(currentEx.javaClass.name, currentEx.localizedMessage, trace) currentEx.javaClass.name,
currentEx.localizedMessage,
trace
)
return@mapTo Error(errorInternal, logger) return@mapTo Error(errorInternal, logger)
} }

@ -10,6 +10,7 @@ import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@ -50,8 +51,8 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
} }
/** /**
* The Throwable object that caused the event in your application. * The {@link Throwable} object that caused the event in your application.
* * <p>
* Manipulating this field does not affect the error information reported to the * Manipulating this field does not affect the error information reported to the
* Bugsnag dashboard. Use {@link Event#getErrors()} to access and amend the representation of * Bugsnag dashboard. Use {@link Event#getErrors()} to access and amend the representation of
* the error that will be sent. * the error that will be sent.
@ -65,7 +66,7 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
* Information extracted from the {@link Throwable} that caused the event can be found in this * Information extracted from the {@link Throwable} that caused the event can be found in this
* field. The list contains at least one {@link Error} that represents the thrown object * field. The list contains at least one {@link Error} that represents the thrown object
* with subsequent elements in the list populated from {@link Throwable#getCause()}. * with subsequent elements in the list populated from {@link Throwable#getCause()}.
* * <p>
* A reference to the actual {@link Throwable} object that caused the event is available * A reference to the actual {@link Throwable} object that caused the event is available
* through {@link Event#getOriginalError()} ()}. * through {@link Event#getOriginalError()} ()}.
*/ */
@ -74,6 +75,35 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
return impl.getErrors(); return impl.getErrors();
} }
/**
* Add a new error to this event and return its Error data. The new Error will appear at the
* end of the {@link #getErrors() errors list}.
*/
@NonNull
public Error addError(@NonNull Throwable error) {
return impl.addError(error);
}
/**
* Add a new empty {@link ErrorType#ANDROID android} error to this event and return its Error
* data. The new Error will appear at the end of the {@link #getErrors() errors list}.
*/
@NonNull
public Error addError(@NonNull String errorClass, @Nullable String errorMessage) {
return impl.addError(errorClass, errorMessage, ErrorType.ANDROID);
}
/**
* Add a new empty error to this event and return its Error data. The new Error will appear
* at the end of the {@link #getErrors() errors list}.
*/
@NonNull
public Error addError(@NonNull String errorClass,
@Nullable String errorMessage,
@NonNull ErrorType errorType) {
return impl.addError(errorClass, errorMessage, errorType);
}
/** /**
* If thread state is being captured along with the event, this field will contain a * If thread state is being captured along with the event, this field will contain a
* list of {@link Thread} objects. * list of {@link Thread} objects.
@ -83,6 +113,46 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
return impl.getThreads(); return impl.getThreads();
} }
/**
* Create, add and return a new empty {@link Thread} object to this event with a given id
* and name. This can be used to augment the event with thread data that would not be picked
* up as part of a normal event being generated (for example: native threads managed
* by cross-platform toolkits).
*
* @return a new Thread object of type {@link ErrorType#ANDROID} with no stacktrace
*/
@NonNull
public Thread addThread(@NonNull String id,
@NonNull String name) {
return impl.addThread(
id,
name,
ErrorType.ANDROID,
false,
Thread.State.RUNNABLE.getDescriptor()
);
}
/**
* Create, add and return a new empty {@link Thread} object to this event with a given id
* and name. This can be used to augment the event with thread data that would not be picked
* up as part of a normal event being generated (for example: native threads managed
* by cross-platform toolkits).
*
* @return a new Thread object of type {@link ErrorType#ANDROID} with no stacktrace
*/
@NonNull
public Thread addThread(long id,
@NonNull String name) {
return impl.addThread(
Long.toString(id),
name,
ErrorType.ANDROID,
false,
Thread.State.RUNNABLE.getDescriptor()
);
}
/** /**
* A list of breadcrumbs leading up to the event. These values can be accessed and amended * A list of breadcrumbs leading up to the event. These values can be accessed and amended
* if necessary. See {@link Breadcrumb} for details of the data available. * if necessary. See {@link Breadcrumb} for details of the data available.
@ -92,6 +162,26 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
return impl.getBreadcrumbs(); return impl.getBreadcrumbs();
} }
/**
* Add a new breadcrumb to this event and return its Breadcrumb object. The new breadcrumb
* will be added to the end of the {@link #getBreadcrumbs() breadcrumbs list} by this method.
*/
@NonNull
public Breadcrumb leaveBreadcrumb(@NonNull String message,
@NonNull BreadcrumbType type,
@Nullable Map<String, Object> metadata) {
return impl.leaveBreadcrumb(message, type, metadata);
}
/**
* Add a new breadcrumb to this event and return its Breadcrumb object. The new breadcrumb
* will be added to the end of the {@link #getBreadcrumbs() breadcrumbs list} by this# method.
*/
@NonNull
public Breadcrumb leaveBreadcrumb(@NonNull String message) {
return impl.leaveBreadcrumb(message, BreadcrumbType.MANUAL, null);
}
/** /**
* A list of feature flags active at the time of the event. * A list of feature flags active at the time of the event.
* See {@link FeatureFlag} for details of the data available. * See {@link FeatureFlag} for details of the data available.
@ -166,7 +256,7 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
* All events with the same grouping hash will be grouped together into one error. This is an * All events with the same grouping hash will be grouped together into one error. This is an
* advanced usage of the library and mis-using it will cause your events not to group properly * advanced usage of the library and mis-using it will cause your events not to group properly
* in your dashboard. * in your dashboard.
* * <p>
* As the name implies, this option accepts a hash of sorts. * As the name implies, this option accepts a hash of sorts.
*/ */
public void setGroupingHash(@Nullable String groupingHash) { public void setGroupingHash(@Nullable String groupingHash) {
@ -178,7 +268,7 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
* All events with the same grouping hash will be grouped together into one error. This is an * All events with the same grouping hash will be grouped together into one error. This is an
* advanced usage of the library and mis-using it will cause your events not to group properly * advanced usage of the library and mis-using it will cause your events not to group properly
* in your dashboard. * in your dashboard.
* * <p>
* As the name implies, this option accepts a hash of sorts. * As the name implies, this option accepts a hash of sorts.
*/ */
@Nullable @Nullable
@ -382,6 +472,21 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
impl.setUnhandled(unhandled); impl.setUnhandled(unhandled);
} }
/**
* Associate this event with a specific trace. This is usually done automatically when
* using bugsnag-android-performance, but can also be set manually if required.
*
* @param traceId the ID of the trace the event occurred within
* @param spanId the ID of the span that the event occurred within
*/
public void setTraceCorrelation(@NonNull UUID traceId, long spanId) {
if (traceId != null) {
impl.setTraceCorrelation(new TraceCorrelation(traceId, spanId));
} else {
logNull("traceId");
}
}
protected boolean shouldDiscardClass() { protected boolean shouldDiscardClass() {
return impl.shouldDiscardClass(); return impl.shouldDiscardClass();
} }

@ -6,6 +6,7 @@ import com.bugsnag.android.internal.InternalMetricsNoop
import com.bugsnag.android.internal.JsonHelper import com.bugsnag.android.internal.JsonHelper
import com.bugsnag.android.internal.TrimMetrics import com.bugsnag.android.internal.TrimMetrics
import java.io.IOException import java.io.IOException
import java.util.Date
import java.util.regex.Pattern import java.util.regex.Pattern
internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware { internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware {
@ -119,6 +120,8 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
*/ */
internal var userImpl: User internal var userImpl: User
var traceCorrelation: TraceCorrelation? = null
fun getUnhandledOverridden(): Boolean = severityReason.unhandledOverridden fun getUnhandledOverridden(): Boolean = severityReason.unhandledOverridden
fun getOriginalUnhandled(): Boolean = severityReason.originalUnhandled fun getOriginalUnhandled(): Boolean = severityReason.originalUnhandled
@ -193,6 +196,10 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
writer.name("featureFlags").value(featureFlags) writer.name("featureFlags").value(featureFlags)
traceCorrelation?.let { correlation ->
writer.name("correlation").value(correlation)
}
if (session != null) { if (session != null) {
val copy = Session.copySession(session) val copy = Session.copySession(session)
writer.name("session").beginObject() writer.name("session").beginObject()
@ -318,4 +325,72 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
override fun clearFeatureFlag(name: String) = featureFlags.clearFeatureFlag(name) override fun clearFeatureFlag(name: String) = featureFlags.clearFeatureFlag(name)
override fun clearFeatureFlags() = featureFlags.clearFeatureFlags() override fun clearFeatureFlags() = featureFlags.clearFeatureFlags()
fun addError(thrownError: Throwable?): Error {
if (thrownError == null) {
val newError = Error(
ErrorInternal("null", null, Stacktrace(ArrayList())),
logger
)
errors.add(newError)
return newError
} else {
val newErrors = Error.createError(thrownError, projectPackages, logger)
errors.addAll(newErrors)
return newErrors.first()
}
}
fun addError(errorClass: String?, errorMessage: String?, errorType: ErrorType?): Error {
val error = Error(
ErrorInternal(
errorClass.toString(),
errorMessage,
Stacktrace(ArrayList()),
errorType ?: ErrorType.ANDROID
),
logger
)
errors.add(error)
return error
}
fun addThread(
id: String?,
name: String?,
errorType: ErrorType,
isErrorReportingThread: Boolean,
state: String
): Thread {
val thread = Thread(
ThreadInternal(
id.toString(),
name.toString(),
errorType,
isErrorReportingThread,
state,
Stacktrace(ArrayList())
),
logger
)
threads.add(thread)
return thread
}
fun leaveBreadcrumb(
message: String?,
type: BreadcrumbType?,
metadata: MutableMap<String, Any?>?
): Breadcrumb {
val breadcrumb = Breadcrumb(
message.toString(),
type ?: BreadcrumbType.MANUAL,
metadata,
Date(),
logger
)
breadcrumbs.add(breadcrumb)
return breadcrumb
}
} }

@ -196,6 +196,7 @@ internal class EventStore(
return null return null
} }
} catch (ioe: Exception) { } catch (ioe: Exception) {
logger.w("could not parse event payload", ioe)
eventSource.clear() eventSource.clear()
} }
val processedEvent = eventSource.event val processedEvent = eventSource.event
@ -208,7 +209,7 @@ internal class EventStore(
} }
private fun handleEventFlushFailure(exc: Exception, eventFile: File) { private fun handleEventFlushFailure(exc: Exception, eventFile: File) {
delegate?.onErrorIOFailure(exc, eventFile, "Crash Report Deserialization") logger.e(exc.message ?: "Failed to send event", exc)
deleteStoredFiles(setOf(eventFile)) deleteStoredFiles(setOf(eventFile))
} }

@ -14,7 +14,7 @@ import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
internal abstract class FileStore( internal abstract class FileStore(
private val storageDir: File, val storageDir: File,
private val maxStoreCount: Int, private val maxStoreCount: Int,
private val comparator: Comparator<in File?>, private val comparator: Comparator<in File?>,
protected open val logger: Logger, protected open val logger: Logger,

@ -21,6 +21,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* Used as the entry point for native code to allow proguard to obfuscate other areas if needed * Used as the entry point for native code to allow proguard to obfuscate other areas if needed
*/ */
@ -430,6 +431,25 @@ public class NativeInterface {
} }
} }
/**
* Attempt to deliver an existing event file that is not current enqueued for delivery. The
* filename is expected to be in the standard {@link EventFilenameInfo} format, and the file
* should contain a correctly formatted {@link Event} object. This method will attempt to
* move the file into place, and flush the queue asynchronously. If the file cannot be moved
* into the queue directory, the file is deleted before returning.
*
* @param reportFile the file to enqueue for delivery
*/
public static void deliverReport(@NonNull File reportFile) {
EventStore eventStore = getClient().eventStore;
File eventFile = new File(eventStore.getStorageDir(), reportFile.getName());
if (reportFile.renameTo(eventFile)) {
eventStore.flushAsync();
} else {
reportFile.delete();
}
}
/** /**
* Notifies using the Android SDK * Notifies using the Android SDK
* *

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

@ -28,11 +28,11 @@ public final class Session implements JsonStream.Streamable, UserAware {
private App app; private App app;
private Device device; private Device device;
private final AtomicBoolean autoCaptured = new AtomicBoolean(false); private volatile boolean autoCaptured = false;
private final AtomicInteger unhandledCount = new AtomicInteger(); private final AtomicInteger unhandledCount = new AtomicInteger();
private final AtomicInteger handledCount = new AtomicInteger(); private final AtomicInteger handledCount = new AtomicInteger();
private final AtomicBoolean tracked = new AtomicBoolean(false); private final AtomicBoolean tracked = new AtomicBoolean(false);
final AtomicBoolean isPaused = new AtomicBoolean(false); private final AtomicBoolean isPaused = new AtomicBoolean(false);
private String apiKey; private String apiKey;
@ -41,7 +41,7 @@ public final class Session implements JsonStream.Streamable, UserAware {
session.unhandledCount.get(), session.handledCount.get(), session.notifier, session.unhandledCount.get(), session.handledCount.get(), session.notifier,
session.logger, session.getApiKey()); session.logger, session.getApiKey());
copy.tracked.set(session.tracked.get()); copy.tracked.set(session.tracked.get());
copy.autoCaptured.set(session.isAutoCaptured()); copy.autoCaptured = session.isAutoCaptured();
return copy; return copy;
} }
@ -68,7 +68,7 @@ public final class Session implements JsonStream.Streamable, UserAware {
this.id = id; this.id = id;
this.startedAt = new Date(startedAt.getTime()); this.startedAt = new Date(startedAt.getTime());
this.user = user; this.user = user;
this.autoCaptured.set(autoCaptured); this.autoCaptured = autoCaptured;
this.apiKey = apiKey; this.apiKey = apiKey;
} }
@ -198,16 +198,28 @@ public final class Session implements JsonStream.Streamable, UserAware {
return copySession(this); return copySession(this);
} }
AtomicBoolean isTracked() { boolean markTracked() {
return tracked; return tracked.compareAndSet(false, true);
}
boolean markResumed() {
return isPaused.compareAndSet(true, false);
}
void markPaused() {
isPaused.set(true);
}
boolean isPaused() {
return isPaused.get();
} }
boolean isAutoCaptured() { boolean isAutoCaptured() {
return autoCaptured.get(); return autoCaptured;
} }
void setAutoCaptured(boolean autoCaptured) { void setAutoCaptured(boolean autoCaptured) {
this.autoCaptured.set(autoCaptured); this.autoCaptured = autoCaptured;
} }
/** /**

@ -36,7 +36,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
private volatile Session currentSession = null; private volatile Session currentSession = null;
final BackgroundTaskService backgroundTaskService; final BackgroundTaskService backgroundTaskService;
final Logger logger; final Logger logger;
private boolean shouldSuppressFirstAutoSession = false; private boolean shouldSuppressFirstAutoSession = true;
SessionTracker(ImmutableConfig configuration, SessionTracker(ImmutableConfig configuration,
CallbackState callbackState, CallbackState callbackState,
@ -108,9 +108,13 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
&& existingSession != null && existingSession != null
&& !existingSession.isAutoCaptured() && !existingSession.isAutoCaptured()
&& shouldSuppressFirstAutoSession) { && shouldSuppressFirstAutoSession) {
shouldSuppressFirstAutoSession = true; shouldSuppressFirstAutoSession = false;
return true; return true;
} }
if (autoCaptured) {
shouldSuppressFirstAutoSession = false;
}
} }
return false; return false;
} }
@ -120,7 +124,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
Session session = currentSession; Session session = currentSession;
if (session != null) { if (session != null) {
session.isPaused.set(true); session.markPaused();
updateState(StateEvent.PauseSession.INSTANCE); updateState(StateEvent.PauseSession.INSTANCE);
} }
} }
@ -133,7 +137,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
session = startSession(false); session = startSession(false);
resumed = false; resumed = false;
} else { } else {
resumed = session.isPaused.compareAndSet(true, false); resumed = session.markResumed();
} }
if (session != null) { if (session != null) {
@ -191,7 +195,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
session.setDevice(client.getDeviceDataCollector().generateDevice()); session.setDevice(client.getDeviceDataCollector().generateDevice());
boolean deliverSession = callbackState.runOnSessionTasks(session, logger); boolean deliverSession = callbackState.runOnSessionTasks(session, logger);
if (deliverSession && session.isTracked().compareAndSet(false, true)) { if (deliverSession && session.markTracked()) {
currentSession = session; currentSession = session;
notifySessionStartObserver(session); notifySessionStartObserver(session);
flushInMemorySession(session); flushInMemorySession(session);
@ -205,7 +209,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
Session getCurrentSession() { Session getCurrentSession() {
Session session = currentSession; Session session = currentSession;
if (session != null && !session.isPaused.get()) { if (session != null && !session.isPaused()) {
return session; return session;
} }
return null; return null;

@ -1,6 +1,7 @@
package com.bugsnag.android package com.bugsnag.android
import java.io.IOException import java.io.IOException
import kotlin.math.min
/** /**
* Serialize an exception stacktrace and mark frames as "in-project" * Serialize an exception stacktrace and mark frames as "in-project"
@ -25,11 +26,35 @@ internal class Stacktrace : JsonStream.Streamable {
else -> null else -> null
} }
} }
fun serializeStackframe(
el: StackTraceElement,
projectPackages: Collection<String>,
logger: Logger
): Stackframe? {
try {
val className = el.className
val methodName = when {
className.isNotEmpty() -> className + "." + el.methodName
else -> el.methodName
}
return Stackframe(
methodName,
el.fileName ?: "Unknown",
el.lineNumber,
inProject(className, projectPackages)
)
} catch (lineEx: Exception) {
logger.w("Failed to serialize stacktrace", lineEx)
return null
}
}
} }
val trace: List<Stackframe> val trace: MutableList<Stackframe>
constructor(frames: List<Stackframe>) { constructor(frames: MutableList<Stackframe>) {
trace = limitTraceLength(frames) trace = limitTraceLength(frames)
} }
@ -38,48 +63,26 @@ internal class Stacktrace : JsonStream.Streamable {
projectPackages: Collection<String>, projectPackages: Collection<String>,
logger: Logger logger: Logger
) { ) {
val frames = limitTraceLength(stacktrace) // avoid allocating new subLists or Arrays by only copying the required number of frames
trace = frames.mapNotNull { serializeStackframe(it, projectPackages, logger) } // mapping them to our internal Stackframes as we go, roughly equivalent to
} // stacktrace.take(STACKTRACE_TRIM_LENGTH).mapNotNullTo(ArrayList()) { ... }
val frameCount = min(STACKTRACE_TRIM_LENGTH, stacktrace.size)
private fun limitTraceLength(frames: Array<StackTraceElement>): Array<StackTraceElement> { trace = ArrayList(frameCount)
return when { for (i in 0 until frameCount) {
frames.size >= STACKTRACE_TRIM_LENGTH -> frames.sliceArray(0 until STACKTRACE_TRIM_LENGTH) val frame = serializeStackframe(stacktrace[i], projectPackages, logger)
else -> frames if (frame != null) {
trace.add(frame)
}
} }
} }
private fun limitTraceLength(frames: List<Stackframe>): List<Stackframe> { private fun limitTraceLength(frames: MutableList<Stackframe>): MutableList<Stackframe> {
return when { return when {
frames.size >= STACKTRACE_TRIM_LENGTH -> frames.subList(0, STACKTRACE_TRIM_LENGTH) frames.size >= STACKTRACE_TRIM_LENGTH -> frames.subList(0, STACKTRACE_TRIM_LENGTH)
else -> frames else -> frames
} }
} }
private fun serializeStackframe(
el: StackTraceElement,
projectPackages: Collection<String>,
logger: Logger
): Stackframe? {
try {
val className = el.className
val methodName = when {
className.isNotEmpty() -> className + "." + el.methodName
else -> el.methodName
}
return Stackframe(
methodName,
el.fileName ?: "Unknown",
el.lineNumber,
inProject(className, projectPackages)
)
} catch (lineEx: Exception) {
logger.w("Failed to serialize stacktrace", lineEx)
return null
}
}
@Throws(IOException::class) @Throws(IOException::class)
override fun toStream(writer: JsonStream) { override fun toStream(writer: JsonStream) {
writer.beginArray() writer.beginArray()

@ -10,7 +10,8 @@ sealed class StateEvent { // JvmField allows direct field access optimizations
@JvmField val releaseStage: String?, @JvmField val releaseStage: String?,
@JvmField val lastRunInfoPath: String, @JvmField val lastRunInfoPath: String,
@JvmField val consecutiveLaunchCrashes: Int, @JvmField val consecutiveLaunchCrashes: Int,
@JvmField val sendThreads: ThreadSendPolicy @JvmField val sendThreads: ThreadSendPolicy,
@JvmField val maxBreadcrumbs: Int
) : StateEvent() ) : StateEvent()
object DeliverPending : StateEvent() object DeliverPending : StateEvent()

@ -160,6 +160,16 @@ public class Thread implements JsonStream.Streamable {
return impl.getStacktrace(); return impl.getStacktrace();
} }
/**
* Add a new stackframe to the end of this thread returning the new Stackframe data object.
*/
@NonNull
public Stackframe addStackframe(@Nullable String method,
@Nullable String file,
long lineNumber) {
return impl.addStackframe(method, file, lineNumber);
}
@Override @Override
public void toStream(@NonNull JsonStream stream) throws IOException { public void toStream(@NonNull JsonStream stream) throws IOException {
impl.toStream(stream); impl.toStream(stream);

@ -13,6 +13,12 @@ class ThreadInternal internal constructor(
var stacktrace: MutableList<Stackframe> = stacktrace.trace.toMutableList() var stacktrace: MutableList<Stackframe> = stacktrace.trace.toMutableList()
fun addStackframe(method: String?, file: String?, lineNumber: Long): Stackframe {
val frame = Stackframe(method, file, lineNumber, null)
stacktrace.add(frame)
return frame
}
@Throws(IOException::class) @Throws(IOException::class)
override fun toStream(writer: JsonStream) { override fun toStream(writer: JsonStream) {
writer.beginObject() writer.beginObject()

@ -0,0 +1,20 @@
package com.bugsnag.android
import java.util.UUID
internal data class TraceCorrelation(val traceId: UUID, val spanId: Long) : JsonStream.Streamable {
override fun toStream(writer: JsonStream) {
writer.beginObject()
.name("traceId").value(traceId.toHexString())
.name("spanId").value(spanId.toHexString())
writer.endObject()
}
private fun UUID.toHexString(): String {
return "%016x%016x".format(mostSignificantBits, leastSignificantBits)
}
private fun Long.toHexString(): String {
return "%016x".format(this)
}
}

@ -53,7 +53,7 @@ class InternalMetricsImpl(source: Map<String, Any>? = null) : InternalMetrics {
// This is currently the only place where we set static data. // This is currently the only place where we set static data.
// When that changes in future, we'll need a StaticData object to properly merge data // When that changes in future, we'll need a StaticData object to properly merge data
// coming from multiple sources. // coming from multiple sources.
NdkPluginCaller.setStaticData(mapOf("usage" to mapOf("config" to configDifferences))) NdkPluginCaller.setStaticData(mapOf("config" to configDifferences))
} }
override fun setCallbackCounts(newCallbackCounts: Map<String, Int>) { override fun setCallbackCounts(newCallbackCounts: Map<String, Int>) {

Loading…
Cancel
Save