mirror of https://github.com/M66B/FairEmail.git
Updated Bugsnag to version 5.23.0
This commit is contained in:
parent
dd8bd36712
commit
f5604d6ede
|
@ -359,7 +359,7 @@ dependencies {
|
||||||
def dnsjava_version = "2.1.9"
|
def dnsjava_version = "2.1.9"
|
||||||
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 = "5.19.2"
|
def bugsnag_version = "5.23.0"
|
||||||
def biweekly_version = "0.6.6"
|
def biweekly_version = "0.6.6"
|
||||||
def vcard_version = "0.11.3"
|
def vcard_version = "0.11.3"
|
||||||
def relinker_version = "1.4.3"
|
def relinker_version = "1.4.3"
|
||||||
|
|
|
@ -158,18 +158,13 @@ internal class BackgroundTaskService(
|
||||||
internalReportExecutor.shutdownNow()
|
internalReportExecutor.shutdownNow()
|
||||||
defaultExecutor.shutdownNow()
|
defaultExecutor.shutdownNow()
|
||||||
|
|
||||||
// shutdown the error/session executors first, waiting for existing tasks to complete.
|
// Wait a little while for these ones to shut down
|
||||||
// If a request fails it may perform IO to persist the payload for delivery next launch,
|
|
||||||
// which would submit tasks to the IO executor - therefore it's critical to
|
|
||||||
// shutdown the IO executor last.
|
|
||||||
errorExecutor.shutdown()
|
errorExecutor.shutdown()
|
||||||
sessionExecutor.shutdown()
|
sessionExecutor.shutdown()
|
||||||
|
ioExecutor.shutdown()
|
||||||
|
|
||||||
errorExecutor.awaitTerminationSafe()
|
errorExecutor.awaitTerminationSafe()
|
||||||
sessionExecutor.awaitTerminationSafe()
|
sessionExecutor.awaitTerminationSafe()
|
||||||
|
|
||||||
// shutdown the IO executor last, waiting for any existing tasks to complete
|
|
||||||
ioExecutor.shutdown()
|
|
||||||
ioExecutor.awaitTerminationSafe()
|
ioExecutor.awaitTerminationSafe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,15 @@ public final class Bugsnag {
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if one of the <code>start</code> methods have been has been called and
|
||||||
|
* so Bugsnag is initialized; false if <code>start</code> has not been called and the
|
||||||
|
* other methods will throw IllegalStateException.
|
||||||
|
*/
|
||||||
|
public static boolean isStarted() {
|
||||||
|
return client != null;
|
||||||
|
}
|
||||||
|
|
||||||
private static void logClientInitWarning() {
|
private static void logClientInitWarning() {
|
||||||
getClient().logger.w("Multiple Bugsnag.start calls detected. Ignoring.");
|
getClient().logger.w("Multiple Bugsnag.start calls detected. Ignoring.");
|
||||||
}
|
}
|
||||||
|
@ -76,18 +85,19 @@ public final class Bugsnag {
|
||||||
/**
|
/**
|
||||||
* Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
|
* Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
|
||||||
* represent what was happening in your application at the time an error occurs.
|
* represent what was happening in your application at the time an error occurs.
|
||||||
*
|
* <p>
|
||||||
* In an android app the "context" is automatically set as the foreground Activity.
|
* In an android app the "context" is automatically set as the foreground Activity.
|
||||||
* If you would like to set this value manually, you should alter this property.
|
* If you would like to set this value manually, you should alter this property.
|
||||||
*/
|
*/
|
||||||
@Nullable public static String getContext() {
|
@Nullable
|
||||||
|
public static String getContext() {
|
||||||
return getClient().getContext();
|
return getClient().getContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
|
* Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
|
||||||
* represent what was happening in your application at the time an error occurs.
|
* represent what was happening in your application at the time an error occurs.
|
||||||
*
|
* <p>
|
||||||
* In an android app the "context" is automatically set as the foreground Activity.
|
* In an android app the "context" is automatically set as the foreground Activity.
|
||||||
* If you would like to set this value manually, you should alter this property.
|
* If you would like to set this value manually, you should alter this property.
|
||||||
*/
|
*/
|
||||||
|
@ -115,15 +125,15 @@ public final class Bugsnag {
|
||||||
/**
|
/**
|
||||||
* Add a "on error" callback, to execute code at the point where an error report is
|
* Add a "on error" callback, to execute code at the point where an error report is
|
||||||
* captured in Bugsnag.
|
* captured in Bugsnag.
|
||||||
*
|
* <p>
|
||||||
* You can use this to add or modify information attached to an Event
|
* You can use this to add or modify information attached to an Event
|
||||||
* before it is sent to your dashboard. You can also return
|
* before it is sent to your dashboard. You can also return
|
||||||
* <code>false</code> from any callback to prevent delivery. "on error"
|
* <code>false</code> from any callback to prevent delivery. "on error"
|
||||||
* callbacks do not run before reports generated in the event
|
* callbacks do not run before reports generated in the event
|
||||||
* of immediate app termination from crashes in C/C++ code.
|
* of immediate app termination from crashes in C/C++ code.
|
||||||
*
|
* <p>
|
||||||
* For example:
|
* For example:
|
||||||
*
|
* <p>
|
||||||
* Bugsnag.addOnError(new OnErrorCallback() {
|
* Bugsnag.addOnError(new OnErrorCallback() {
|
||||||
* public boolean run(Event event) {
|
* public boolean run(Event event) {
|
||||||
* event.setSeverity(Severity.INFO);
|
* event.setSeverity(Severity.INFO);
|
||||||
|
@ -140,6 +150,7 @@ public final class Bugsnag {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a previously added "on error" callback
|
* Removes a previously added "on error" callback
|
||||||
|
*
|
||||||
* @param onError the callback to remove
|
* @param onError the callback to remove
|
||||||
*/
|
*/
|
||||||
public static void removeOnError(@NonNull OnErrorCallback onError) {
|
public static void removeOnError(@NonNull OnErrorCallback onError) {
|
||||||
|
@ -149,12 +160,12 @@ public final class Bugsnag {
|
||||||
/**
|
/**
|
||||||
* Add an "on breadcrumb" callback, to execute code before every
|
* Add an "on breadcrumb" callback, to execute code before every
|
||||||
* breadcrumb captured by Bugsnag.
|
* breadcrumb captured by Bugsnag.
|
||||||
*
|
* <p>
|
||||||
* You can use this to modify breadcrumbs before they are stored by Bugsnag.
|
* You can use this to modify breadcrumbs before they are stored by Bugsnag.
|
||||||
* You can also return <code>false</code> from any callback to ignore a breadcrumb.
|
* You can also return <code>false</code> from any callback to ignore a breadcrumb.
|
||||||
*
|
* <p>
|
||||||
* For example:
|
* For example:
|
||||||
*
|
* <p>
|
||||||
* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
|
* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
|
||||||
* public boolean run(Breadcrumb breadcrumb) {
|
* public boolean run(Breadcrumb breadcrumb) {
|
||||||
* return false; // ignore the breadcrumb
|
* return false; // ignore the breadcrumb
|
||||||
|
@ -170,6 +181,7 @@ public final class Bugsnag {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a previously added "on breadcrumb" callback
|
* Removes a previously added "on breadcrumb" callback
|
||||||
|
*
|
||||||
* @param onBreadcrumb the callback to remove
|
* @param onBreadcrumb the callback to remove
|
||||||
*/
|
*/
|
||||||
public static void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
|
public static void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
|
||||||
|
@ -179,12 +191,12 @@ public final class Bugsnag {
|
||||||
/**
|
/**
|
||||||
* Add an "on session" callback, to execute code before every
|
* Add an "on session" callback, to execute code before every
|
||||||
* session captured by Bugsnag.
|
* session captured by Bugsnag.
|
||||||
*
|
* <p>
|
||||||
* You can use this to modify sessions before they are stored by Bugsnag.
|
* You can use this to modify sessions before they are stored by Bugsnag.
|
||||||
* You can also return <code>false</code> from any callback to ignore a session.
|
* You can also return <code>false</code> from any callback to ignore a session.
|
||||||
*
|
* <p>
|
||||||
* For example:
|
* For example:
|
||||||
*
|
* <p>
|
||||||
* Bugsnag.onSession(new OnSessionCallback() {
|
* Bugsnag.onSession(new OnSessionCallback() {
|
||||||
* public boolean run(Session session) {
|
* public boolean run(Session session) {
|
||||||
* return false; // ignore the session
|
* return false; // ignore the session
|
||||||
|
@ -200,6 +212,7 @@ public final class Bugsnag {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a previously added "on session" callback
|
* Removes a previously added "on session" callback
|
||||||
|
*
|
||||||
* @param onSession the callback to remove
|
* @param onSession the callback to remove
|
||||||
*/
|
*/
|
||||||
public static void removeOnSession(@NonNull OnSessionCallback onSession) {
|
public static void removeOnSession(@NonNull OnSessionCallback onSession) {
|
||||||
|
@ -219,7 +232,7 @@ public final class Bugsnag {
|
||||||
* Notify Bugsnag of a handled exception
|
* Notify Bugsnag of a handled exception
|
||||||
*
|
*
|
||||||
* @param exception the exception to send to Bugsnag
|
* @param exception the exception to send to Bugsnag
|
||||||
* @param onError callback invoked on the generated error report for
|
* @param onError callback invoked on the generated error report for
|
||||||
* additional modification
|
* additional modification
|
||||||
*/
|
*/
|
||||||
public static void notify(@NonNull final Throwable exception,
|
public static void notify(@NonNull final Throwable exception,
|
||||||
|
@ -286,7 +299,8 @@ public final class Bugsnag {
|
||||||
/**
|
/**
|
||||||
* Leave a "breadcrumb" log message representing an action or event which
|
* Leave a "breadcrumb" log message representing an action or event which
|
||||||
* occurred in your app, to aid with debugging
|
* occurred in your app, to aid with debugging
|
||||||
* @param message A short label
|
*
|
||||||
|
* @param message A short label
|
||||||
* @param metadata Additional diagnostic information about the app environment
|
* @param metadata Additional diagnostic information about the app environment
|
||||||
* @param type A category for the breadcrumb
|
* @param type A category for the breadcrumb
|
||||||
*/
|
*/
|
||||||
|
@ -332,11 +346,10 @@ public final class Bugsnag {
|
||||||
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
|
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
|
||||||
* stability score</a>.
|
* stability score</a>.
|
||||||
*
|
*
|
||||||
|
* @return true if a previous session was resumed, false if a new session was started.
|
||||||
* @see #startSession()
|
* @see #startSession()
|
||||||
* @see #pauseSession()
|
* @see #pauseSession()
|
||||||
* @see Configuration#setAutoTrackSessions(boolean)
|
* @see Configuration#setAutoTrackSessions(boolean)
|
||||||
*
|
|
||||||
* @return true if a previous session was resumed, false if a new session was started.
|
|
||||||
*/
|
*/
|
||||||
public static boolean resumeSession() {
|
public static boolean resumeSession() {
|
||||||
return getClient().resumeSession();
|
return getClient().resumeSession();
|
||||||
|
@ -365,7 +378,7 @@ public final class Bugsnag {
|
||||||
* Returns the current buffer of breadcrumbs that will be sent with captured events. This
|
* Returns the current buffer of breadcrumbs that will be sent with captured events. This
|
||||||
* ordered list represents the most recent breadcrumbs to be captured up to the limit
|
* ordered list represents the most recent breadcrumbs to be captured up to the limit
|
||||||
* set in {@link Configuration#getMaxBreadcrumbs()}.
|
* set in {@link Configuration#getMaxBreadcrumbs()}.
|
||||||
*
|
* <p>
|
||||||
* The returned collection is readonly and mutating the list will cause no effect on the
|
* The returned collection is readonly and mutating the list will cause no effect on the
|
||||||
* Client's state. If you wish to alter the breadcrumbs collected by the Client then you should
|
* Client's state. If you wish to alter the breadcrumbs collected by the Client then you should
|
||||||
* use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and
|
* use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and
|
||||||
|
@ -380,7 +393,7 @@ public final class Bugsnag {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves information about the last launch of the application, if it has been run before.
|
* Retrieves information about the last launch of the application, if it has been run before.
|
||||||
*
|
* <p>
|
||||||
* For example, this allows checking whether the app crashed on its last launch, which could
|
* For example, this allows checking whether the app crashed on its last launch, which could
|
||||||
* be used to perform conditional behaviour to recover from crashes, such as clearing the
|
* be used to perform conditional behaviour to recover from crashes, such as clearing the
|
||||||
* app data cache.
|
* app data cache.
|
||||||
|
@ -394,7 +407,7 @@ public final class Bugsnag {
|
||||||
* Informs Bugsnag that the application has finished launching. Once this has been called
|
* Informs Bugsnag that the application has finished launching. Once this has been called
|
||||||
* {@link AppWithState#isLaunching()} will always be false in any new error reports,
|
* {@link AppWithState#isLaunching()} will always be false in any new error reports,
|
||||||
* and synchronous delivery will not be attempted on the next launch for any fatal crashes.
|
* and synchronous delivery will not be attempted on the next launch for any fatal crashes.
|
||||||
*
|
* <p>
|
||||||
* By default this method will be called after Bugsnag is initialized when
|
* By default this method will be called after Bugsnag is initialized when
|
||||||
* {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually
|
* {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually
|
||||||
* has precedence over the value supplied via the launchDurationMillis configuration option.
|
* has precedence over the value supplied via the launchDurationMillis configuration option.
|
||||||
|
@ -462,8 +475,12 @@ public final class Bugsnag {
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Client getClient() {
|
public static Client getClient() {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
throw new IllegalStateException("You must call Bugsnag.start before any"
|
synchronized (lock) {
|
||||||
+ " other Bugsnag methods");
|
if (client == null) {
|
||||||
|
throw new IllegalStateException("You must call Bugsnag.start before any"
|
||||||
|
+ " other Bugsnag methods");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
|
|
|
@ -11,6 +11,10 @@ internal class BugsnagEventMapper(
|
||||||
private val logger: Logger
|
private val logger: Logger
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
internal fun convertToEvent(map: Map<in String, Any?>, apiKey: String): Event {
|
||||||
|
return Event(convertToEventImpl(map, apiKey), logger)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
internal fun convertToEventImpl(map: Map<in String, Any?>, apiKey: String): EventInternal {
|
internal fun convertToEventImpl(map: Map<in String, Any?>, apiKey: String): EventInternal {
|
||||||
val event = EventInternal(apiKey)
|
val event = EventInternal(apiKey)
|
||||||
|
@ -85,7 +89,11 @@ internal class BugsnagEventMapper(
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun convertErrorInternal(error: Map<String, Any?>): ErrorInternal {
|
internal fun convertError(error: Map<in String, Any?>): Error {
|
||||||
|
return Error(convertErrorInternal(error), logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun convertErrorInternal(error: Map<in String, Any?>): ErrorInternal {
|
||||||
return ErrorInternal(
|
return ErrorInternal(
|
||||||
error.readEntry("errorClass"),
|
error.readEntry("errorClass"),
|
||||||
error["message"] as? String,
|
error["message"] as? String,
|
||||||
|
|
|
@ -170,7 +170,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
|
|
||||||
DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule,
|
DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule,
|
||||||
configModule, systemServiceModule, trackerModule,
|
configModule, systemServiceModule, trackerModule,
|
||||||
bgTaskService, connectivity, storageModule.getDeviceId(), memoryTrimState);
|
bgTaskService, connectivity, storageModule.getDeviceId(),
|
||||||
|
storageModule.getInternalDeviceId(), memoryTrimState);
|
||||||
dataCollectionModule.resolveDependencies(bgTaskService, TaskType.IO);
|
dataCollectionModule.resolveDependencies(bgTaskService, TaskType.IO);
|
||||||
appDataCollector = dataCollectionModule.getAppDataCollector();
|
appDataCollector = dataCollectionModule.getAppDataCollector();
|
||||||
deviceDataCollector = dataCollectionModule.getDeviceDataCollector();
|
deviceDataCollector = dataCollectionModule.getDeviceDataCollector();
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.bugsnag.android
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
internal class ConfigInternal(
|
internal class ConfigInternal(
|
||||||
var apiKey: String
|
var apiKey: String
|
||||||
|
@ -40,6 +41,7 @@ internal class ConfigInternal(
|
||||||
var maxBreadcrumbs: Int = DEFAULT_MAX_BREADCRUMBS
|
var maxBreadcrumbs: Int = DEFAULT_MAX_BREADCRUMBS
|
||||||
var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS
|
var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS
|
||||||
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
|
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
|
||||||
|
var maxReportedThreads: Int = DEFAULT_MAX_REPORTED_THREADS
|
||||||
var context: String? = null
|
var context: String? = null
|
||||||
|
|
||||||
var redactedKeys: Set<String>
|
var redactedKeys: Set<String>
|
||||||
|
@ -51,6 +53,7 @@ internal class ConfigInternal(
|
||||||
var discardClasses: Set<String> = emptySet()
|
var discardClasses: Set<String> = emptySet()
|
||||||
var enabledReleaseStages: Set<String>? = null
|
var enabledReleaseStages: Set<String>? = null
|
||||||
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
|
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
|
||||||
|
var telemetry: Set<Telemetry> = EnumSet.of(Telemetry.INTERNAL_ERRORS)
|
||||||
var projectPackages: Set<String> = emptySet()
|
var projectPackages: Set<String> = emptySet()
|
||||||
var persistenceDirectory: File? = null
|
var persistenceDirectory: File? = null
|
||||||
|
|
||||||
|
@ -99,6 +102,7 @@ internal class ConfigInternal(
|
||||||
private const val DEFAULT_MAX_BREADCRUMBS = 50
|
private const val DEFAULT_MAX_BREADCRUMBS = 50
|
||||||
private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128
|
private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128
|
||||||
private const val DEFAULT_MAX_PERSISTED_EVENTS = 32
|
private const val DEFAULT_MAX_PERSISTED_EVENTS = 32
|
||||||
|
private const val DEFAULT_MAX_REPORTED_THREADS = 200
|
||||||
private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000
|
private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -561,6 +561,32 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the maximum number of threads that will be reported with an event. Once the threshold is
|
||||||
|
* reached, all remaining threads will be omitted.
|
||||||
|
*
|
||||||
|
* By default, up to 200 threads are reported.
|
||||||
|
*/
|
||||||
|
public int getMaxReportedThreads() {
|
||||||
|
return impl.getMaxReportedThreads();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum number of threads that will be reported with an event. Once the threshold is
|
||||||
|
* reached, all remaining threads will be omitted.
|
||||||
|
*
|
||||||
|
* By default, up to 200 threads are reported.
|
||||||
|
*/
|
||||||
|
public void setMaxReportedThreads(int maxReportedThreads) {
|
||||||
|
if (maxReportedThreads >= 0) {
|
||||||
|
impl.setMaxReportedThreads(maxReportedThreads);
|
||||||
|
} else {
|
||||||
|
getLogger().e("Invalid configuration value detected. "
|
||||||
|
+ "Option maxReportedThreads should be a positive integer."
|
||||||
|
+ "Supplied value is " + maxReportedThreads);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the maximum number of persisted sessions which will be stored. Once the threshold is
|
* Sets the maximum number of persisted sessions which will be stored. Once the threshold is
|
||||||
* reached, the oldest session will be deleted.
|
* reached, the oldest session will be deleted.
|
||||||
|
@ -720,6 +746,26 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
|
||||||
impl.setEnabledBreadcrumbTypes(enabledBreadcrumbTypes);
|
impl.setEnabledBreadcrumbTypes(enabledBreadcrumbTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Set<Telemetry> getTelemetry() {
|
||||||
|
return impl.getTelemetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set which telemetry will be sent to Bugsnag. By default, all telemetry is enabled.
|
||||||
|
*
|
||||||
|
* The following telemetry can be enabled:
|
||||||
|
*
|
||||||
|
* - internal errors: Errors in the Bugsnag SDK itself.
|
||||||
|
*/
|
||||||
|
public void setTelemetry(@NonNull Set<Telemetry> telemetry) {
|
||||||
|
if (telemetry != null) {
|
||||||
|
impl.setTelemetry(telemetry);
|
||||||
|
} else {
|
||||||
|
logNull("telemetry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets which package names Bugsnag should consider as a part of the
|
* Sets which package names Bugsnag should consider as a part of the
|
||||||
* running application. We mark stacktrace lines as in-project if they
|
* running application. We mark stacktrace lines as in-project if they
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.location.LocationManager
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
|
@ -69,3 +70,7 @@ internal fun Context.getConnectivityManager(): ConnectivityManager? =
|
||||||
@JvmName("getStorageManagerFrom")
|
@JvmName("getStorageManagerFrom")
|
||||||
internal fun Context.getStorageManager(): StorageManager? =
|
internal fun Context.getStorageManager(): StorageManager? =
|
||||||
safeGetSystemService(Context.STORAGE_SERVICE)
|
safeGetSystemService(Context.STORAGE_SERVICE)
|
||||||
|
|
||||||
|
@JvmName("getLocationManager")
|
||||||
|
internal fun Context.getLocationManager(): LocationManager? =
|
||||||
|
safeGetSystemService(Context.LOCATION_SERVICE)
|
||||||
|
|
|
@ -18,6 +18,7 @@ internal class DataCollectionModule(
|
||||||
bgTaskService: BackgroundTaskService,
|
bgTaskService: BackgroundTaskService,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
deviceId: String?,
|
deviceId: String?,
|
||||||
|
internalDeviceId: String?,
|
||||||
memoryTrimState: MemoryTrimState
|
memoryTrimState: MemoryTrimState
|
||||||
) : DependencyModule() {
|
) : DependencyModule() {
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ internal class DataCollectionModule(
|
||||||
ctx,
|
ctx,
|
||||||
ctx.resources,
|
ctx.resources,
|
||||||
deviceId,
|
deviceId,
|
||||||
|
internalDeviceId,
|
||||||
deviceBuildInfo,
|
deviceBuildInfo,
|
||||||
dataDir,
|
dataDir,
|
||||||
rootDetector,
|
rootDetector,
|
||||||
|
|
|
@ -13,7 +13,6 @@ import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.HashMap
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
|
@ -28,6 +27,7 @@ internal class DeviceDataCollector(
|
||||||
private val appContext: Context,
|
private val appContext: Context,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
private val deviceId: String?,
|
private val deviceId: String?,
|
||||||
|
private val internalDeviceId: String?,
|
||||||
private val buildInfo: DeviceBuildInfo,
|
private val buildInfo: DeviceBuildInfo,
|
||||||
private val dataDirectory: File,
|
private val dataDirectory: File,
|
||||||
rootDetector: RootDetector,
|
rootDetector: RootDetector,
|
||||||
|
@ -42,7 +42,7 @@ internal class DeviceDataCollector(
|
||||||
private val screenResolution = getScreenResolution()
|
private val screenResolution = getScreenResolution()
|
||||||
private val locale = Locale.getDefault().toString()
|
private val locale = Locale.getDefault().toString()
|
||||||
private val cpuAbi = getCpuAbi()
|
private val cpuAbi = getCpuAbi()
|
||||||
private val runtimeVersions: MutableMap<String, Any>
|
private var runtimeVersions: MutableMap<String, Any>
|
||||||
private val rootedFuture: Future<Boolean>?
|
private val rootedFuture: Future<Boolean>?
|
||||||
private val totalMemoryFuture: Future<Long?>? = retrieveTotalDeviceMemory()
|
private val totalMemoryFuture: Future<Long?>? = retrieveTotalDeviceMemory()
|
||||||
private var orientation = AtomicInteger(resources.configuration.orientation)
|
private var orientation = AtomicInteger(resources.configuration.orientation)
|
||||||
|
@ -89,6 +89,19 @@ internal class DeviceDataCollector(
|
||||||
Date(now)
|
Date(now)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun generateInternalDeviceWithState(now: Long) = DeviceWithState(
|
||||||
|
buildInfo,
|
||||||
|
checkIsRooted(),
|
||||||
|
internalDeviceId,
|
||||||
|
locale,
|
||||||
|
totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
|
||||||
|
runtimeVersions.toMutableMap(),
|
||||||
|
calculateFreeDisk(),
|
||||||
|
calculateFreeMemory(),
|
||||||
|
getOrientationAsString(),
|
||||||
|
Date(now)
|
||||||
|
)
|
||||||
|
|
||||||
fun getDeviceMetadata(): Map<String, Any?> {
|
fun getDeviceMetadata(): Map<String, Any?> {
|
||||||
val map = HashMap<String, Any?>()
|
val map = HashMap<String, Any?>()
|
||||||
populateBatteryInfo(into = map)
|
populateBatteryInfo(into = map)
|
||||||
|
@ -163,19 +176,24 @@ internal class DeviceDataCollector(
|
||||||
*/
|
*/
|
||||||
private fun getLocationStatus(): String? {
|
private fun getLocationStatus(): String? {
|
||||||
try {
|
try {
|
||||||
val cr = appContext.contentResolver
|
return if (isLocationEnabled()) "allowed" else "disallowed"
|
||||||
@Suppress("DEPRECATION") val providersAllowed =
|
|
||||||
Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)
|
|
||||||
return when {
|
|
||||||
providersAllowed != null && providersAllowed.isNotEmpty() -> "allowed"
|
|
||||||
else -> "disallowed"
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
logger.w("Could not get locationStatus")
|
logger.w("Could not get locationStatus")
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isLocationEnabled() = when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
|
||||||
|
appContext.getLocationManager()?.isLocationEnabled == true
|
||||||
|
else -> {
|
||||||
|
val cr = appContext.contentResolver
|
||||||
|
@Suppress("DEPRECATION") val providersAllowed =
|
||||||
|
Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)
|
||||||
|
providersAllowed != null && providersAllowed.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current status of network access, eg "cellular"
|
* Get the current status of network access, eg "cellular"
|
||||||
*/
|
*/
|
||||||
|
@ -293,6 +311,9 @@ internal class DeviceDataCollector(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addRuntimeVersionInfo(key: String, value: String) {
|
fun addRuntimeVersionInfo(key: String, value: String) {
|
||||||
runtimeVersions[key] = value
|
// Use copy-on-write to avoid a ConcurrentModificationException in generateDeviceWithState
|
||||||
|
val newRuntimeVersions = runtimeVersions.toMutableMap()
|
||||||
|
newRuntimeVersions[key] = value
|
||||||
|
runtimeVersions = newRuntimeVersions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.lang.Thread
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.nio.channels.FileLock
|
||||||
|
import java.nio.channels.OverlappingFileLockException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for persisting and retrieving a device ID to a file.
|
||||||
|
*
|
||||||
|
* This class is made multi-process safe through the use of a [FileLock], and thread safe
|
||||||
|
* through the use of a [ReadWriteLock] in [SynchronizedStreamableStore].
|
||||||
|
*/
|
||||||
|
class DeviceIdFilePersistence(
|
||||||
|
private val file: File,
|
||||||
|
private val deviceIdGenerator: () -> UUID,
|
||||||
|
private val logger: Logger
|
||||||
|
) : DeviceIdPersistence {
|
||||||
|
private val synchronizedStreamableStore: SynchronizedStreamableStore<DeviceId>
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
file.createNewFile()
|
||||||
|
} catch (exc: Throwable) {
|
||||||
|
logger.w("Failed to created device ID file", exc)
|
||||||
|
}
|
||||||
|
this.synchronizedStreamableStore = SynchronizedStreamableStore(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the device ID from its file system location.
|
||||||
|
* If no value is present then a UUID will be generated and persisted.
|
||||||
|
*/
|
||||||
|
override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean): String? {
|
||||||
|
return try {
|
||||||
|
// optimistically read device ID without a lock - the majority of the time
|
||||||
|
// the device ID will already be present so no synchronization is required.
|
||||||
|
val deviceId = loadDeviceIdInternal()
|
||||||
|
|
||||||
|
if (deviceId?.id != null) {
|
||||||
|
deviceId.id
|
||||||
|
} else {
|
||||||
|
return if (requestCreateIfDoesNotExist) persistNewDeviceUuid(deviceIdGenerator()) else null
|
||||||
|
}
|
||||||
|
} catch (exc: Throwable) {
|
||||||
|
logger.w("Failed to load device ID", exc)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the device ID from the file.
|
||||||
|
*
|
||||||
|
* If the file has zero length it can't contain device ID, so reading will be skipped.
|
||||||
|
*/
|
||||||
|
private fun loadDeviceIdInternal(): DeviceId? {
|
||||||
|
if (file.length() > 0) {
|
||||||
|
try {
|
||||||
|
return synchronizedStreamableStore.load(DeviceId.Companion::fromReader)
|
||||||
|
} catch (exc: Throwable) { // catch AssertionError which can be thrown by JsonReader
|
||||||
|
// on Android 8.0/8.1. see https://issuetracker.google.com/issues/79920590
|
||||||
|
logger.w("Failed to load device ID", exc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a new Device ID to the file.
|
||||||
|
*/
|
||||||
|
private fun persistNewDeviceUuid(uuid: UUID): String? {
|
||||||
|
return try {
|
||||||
|
// acquire a FileLock to prevent Clients in different processes writing
|
||||||
|
// to the same file concurrently
|
||||||
|
file.outputStream().channel.use { channel ->
|
||||||
|
persistNewDeviceIdWithLock(channel, uuid)
|
||||||
|
}
|
||||||
|
} catch (exc: IOException) {
|
||||||
|
logger.w("Failed to persist device ID", exc)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persistNewDeviceIdWithLock(
|
||||||
|
channel: FileChannel,
|
||||||
|
uuid: UUID
|
||||||
|
): String? {
|
||||||
|
val lock = waitForFileLock(channel) ?: return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// read the device ID again as it could have changed
|
||||||
|
// between the last read and when the lock was acquired
|
||||||
|
val deviceId = loadDeviceIdInternal()
|
||||||
|
|
||||||
|
if (deviceId?.id != null) {
|
||||||
|
// the device ID changed between the last read
|
||||||
|
// and acquiring the lock, so return the generated value
|
||||||
|
deviceId.id
|
||||||
|
} else {
|
||||||
|
// generate a new device ID and persist it
|
||||||
|
val newId = DeviceId(uuid.toString())
|
||||||
|
synchronizedStreamableStore.persist(newId)
|
||||||
|
newId.id
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to acquire a file lock. If [OverlappingFileLockException] is thrown
|
||||||
|
* then the method will wait for 50ms then try again, for a maximum of 10 attempts.
|
||||||
|
*/
|
||||||
|
private fun waitForFileLock(channel: FileChannel): FileLock? {
|
||||||
|
repeat(MAX_FILE_LOCK_ATTEMPTS) {
|
||||||
|
try {
|
||||||
|
return channel.tryLock()
|
||||||
|
} catch (exc: OverlappingFileLockException) {
|
||||||
|
Thread.sleep(FILE_LOCK_WAIT_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_FILE_LOCK_ATTEMPTS = 20
|
||||||
|
private const val FILE_LOCK_WAIT_MS = 25L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes and deserializes the device ID to/from JSON.
|
||||||
|
*/
|
||||||
|
private class DeviceId(val id: String?) : JsonStream.Streamable {
|
||||||
|
|
||||||
|
override fun toStream(stream: JsonStream) {
|
||||||
|
with(stream) {
|
||||||
|
beginObject()
|
||||||
|
name(KEY_ID)
|
||||||
|
value(id)
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : JsonReadable<DeviceId> {
|
||||||
|
private const val KEY_ID = "id"
|
||||||
|
|
||||||
|
override fun fromReader(reader: JsonReader): DeviceId {
|
||||||
|
var id: String? = null
|
||||||
|
with(reader) {
|
||||||
|
beginObject()
|
||||||
|
if (hasNext() && KEY_ID == nextName()) {
|
||||||
|
id = nextString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DeviceId(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
interface DeviceIdPersistence {
|
||||||
|
/**
|
||||||
|
* Loads the device ID from storage.
|
||||||
|
*
|
||||||
|
* Device IDs are UUIDs which are persisted on a per-install basis.
|
||||||
|
*
|
||||||
|
* This method must be thread-safe and multi-process safe.
|
||||||
|
*
|
||||||
|
* Note: requestCreateIfDoesNotExist is only a request; an implementation may still refuse to create a new ID.
|
||||||
|
*/
|
||||||
|
fun loadDeviceId(requestCreateIfDoesNotExist: Boolean): String?
|
||||||
|
}
|
|
@ -1,41 +1,33 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.JsonReader
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.Thread
|
|
||||||
import java.nio.channels.FileChannel
|
|
||||||
import java.nio.channels.FileLock
|
|
||||||
import java.nio.channels.OverlappingFileLockException
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is responsible for persisting and retrieving the device ID which uniquely
|
* This class is responsible for persisting and retrieving the device ID and internal device ID,
|
||||||
* identifies this device.
|
* which uniquely identify this device in various contexts.
|
||||||
*
|
|
||||||
* This class is made multi-process safe through the use of a [FileLock], and thread safe
|
|
||||||
* through the use of a [ReadWriteLock] in [SynchronizedStreamableStore].
|
|
||||||
*/
|
*/
|
||||||
internal class DeviceIdStore @JvmOverloads constructor(
|
internal class DeviceIdStore @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val file: File = File(context.filesDir, "device-id"),
|
deviceIdfile: File = File(context.filesDir, "device-id"),
|
||||||
|
deviceIdGenerator: () -> UUID = { UUID.randomUUID() },
|
||||||
|
internalDeviceIdfile: File = File(context.filesDir, "internal-device-id"),
|
||||||
|
internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() },
|
||||||
private val sharedPrefMigrator: SharedPrefMigrator,
|
private val sharedPrefMigrator: SharedPrefMigrator,
|
||||||
private val logger: Logger
|
logger: Logger
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val synchronizedStreamableStore: SynchronizedStreamableStore<DeviceId>
|
private val persistence: DeviceIdPersistence
|
||||||
|
private val internalPersistence: DeviceIdPersistence
|
||||||
|
|
||||||
init {
|
init {
|
||||||
try {
|
persistence = DeviceIdFilePersistence(deviceIdfile, deviceIdGenerator, logger)
|
||||||
file.createNewFile()
|
internalPersistence = DeviceIdFilePersistence(internalDeviceIdfile, internalDeviceIdGenerator, logger)
|
||||||
} catch (exc: Throwable) {
|
|
||||||
logger.w("Failed to created device ID file", exc)
|
|
||||||
}
|
|
||||||
this.synchronizedStreamableStore = SynchronizedStreamableStore(file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Loads the device ID from
|
||||||
* Loads the device ID from its file system location. Device IDs are UUIDs which are
|
* Loads the device ID from its file system location. Device IDs are UUIDs which are
|
||||||
* persisted on a per-install basis. This method is thread-safe and multi-process safe.
|
* persisted on a per-install basis. This method is thread-safe and multi-process safe.
|
||||||
*
|
*
|
||||||
|
@ -43,137 +35,18 @@ internal class DeviceIdStore @JvmOverloads constructor(
|
||||||
* be used. If no value is present then a random UUID will be generated and persisted.
|
* be used. If no value is present then a random UUID will be generated and persisted.
|
||||||
*/
|
*/
|
||||||
fun loadDeviceId(): String? {
|
fun loadDeviceId(): String? {
|
||||||
return loadDeviceId {
|
var result = persistence.loadDeviceId(false)
|
||||||
when (val legacyDeviceId = sharedPrefMigrator.loadDeviceId()) {
|
if (result != null) {
|
||||||
null -> UUID.randomUUID()
|
return result
|
||||||
else -> UUID.fromString(legacyDeviceId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
result = sharedPrefMigrator.loadDeviceId(false)
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return persistence.loadDeviceId(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun loadDeviceId(uuidProvider: () -> UUID): String? {
|
fun loadInternalDeviceId(): String? {
|
||||||
return try {
|
return internalPersistence.loadDeviceId(true)
|
||||||
// optimistically read device ID without a lock - the majority of the time
|
|
||||||
// the device ID will already be present so no synchronization is required.
|
|
||||||
val deviceId = loadDeviceIdInternal()
|
|
||||||
|
|
||||||
if (deviceId?.id != null) {
|
|
||||||
deviceId.id
|
|
||||||
} else {
|
|
||||||
return persistNewDeviceUuid(uuidProvider)
|
|
||||||
}
|
|
||||||
} catch (exc: Throwable) {
|
|
||||||
logger.w("Failed to load device ID", exc)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the device ID from the file.
|
|
||||||
*
|
|
||||||
* If the file has zero length it can't contain device ID, so reading will be skipped.
|
|
||||||
*/
|
|
||||||
private fun loadDeviceIdInternal(): DeviceId? {
|
|
||||||
if (file.length() > 0) {
|
|
||||||
try {
|
|
||||||
return synchronizedStreamableStore.load(DeviceId.Companion::fromReader)
|
|
||||||
} catch (exc: Throwable) { // catch AssertionError which can be thrown by JsonReader
|
|
||||||
// on Android 8.0/8.1. see https://issuetracker.google.com/issues/79920590
|
|
||||||
logger.w("Failed to load device ID", exc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a new Device ID to the file.
|
|
||||||
*/
|
|
||||||
private fun persistNewDeviceUuid(uuidProvider: () -> UUID): String? {
|
|
||||||
return try {
|
|
||||||
// acquire a FileLock to prevent Clients in different processes writing
|
|
||||||
// to the same file concurrently
|
|
||||||
file.outputStream().channel.use { channel ->
|
|
||||||
persistNewDeviceIdWithLock(channel, uuidProvider)
|
|
||||||
}
|
|
||||||
} catch (exc: IOException) {
|
|
||||||
logger.w("Failed to persist device ID", exc)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun persistNewDeviceIdWithLock(
|
|
||||||
channel: FileChannel,
|
|
||||||
uuidProvider: () -> UUID
|
|
||||||
): String? {
|
|
||||||
val lock = waitForFileLock(channel) ?: return null
|
|
||||||
|
|
||||||
return try {
|
|
||||||
// read the device ID again as it could have changed
|
|
||||||
// between the last read and when the lock was acquired
|
|
||||||
val deviceId = loadDeviceIdInternal()
|
|
||||||
|
|
||||||
if (deviceId?.id != null) {
|
|
||||||
// the device ID changed between the last read
|
|
||||||
// and acquiring the lock, so return the generated value
|
|
||||||
deviceId.id
|
|
||||||
} else {
|
|
||||||
// generate a new device ID and persist it
|
|
||||||
val newId = DeviceId(uuidProvider().toString())
|
|
||||||
synchronizedStreamableStore.persist(newId)
|
|
||||||
newId.id
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
lock.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to acquire a file lock. If [OverlappingFileLockException] is thrown
|
|
||||||
* then the method will wait for 50ms then try again, for a maximum of 10 attempts.
|
|
||||||
*/
|
|
||||||
private fun waitForFileLock(channel: FileChannel): FileLock? {
|
|
||||||
repeat(MAX_FILE_LOCK_ATTEMPTS) {
|
|
||||||
try {
|
|
||||||
return channel.tryLock()
|
|
||||||
} catch (exc: OverlappingFileLockException) {
|
|
||||||
Thread.sleep(FILE_LOCK_WAIT_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val MAX_FILE_LOCK_ATTEMPTS = 20
|
|
||||||
private const val FILE_LOCK_WAIT_MS = 25L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes and deserializes the device ID to/from JSON.
|
|
||||||
*/
|
|
||||||
private class DeviceId(val id: String?) : JsonStream.Streamable {
|
|
||||||
|
|
||||||
override fun toStream(stream: JsonStream) {
|
|
||||||
with(stream) {
|
|
||||||
beginObject()
|
|
||||||
name(KEY_ID)
|
|
||||||
value(id)
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object : JsonReadable<DeviceId> {
|
|
||||||
private const val KEY_ID = "id"
|
|
||||||
|
|
||||||
override fun fromReader(reader: JsonReader): DeviceId {
|
|
||||||
var id: String? = null
|
|
||||||
with(reader) {
|
|
||||||
beginObject()
|
|
||||||
if (hasNext() && KEY_ID == nextName()) {
|
|
||||||
id = nextString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DeviceId(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,16 @@ enum class ErrorType(internal val desc: String) {
|
||||||
/**
|
/**
|
||||||
* An error captured from Android's C layer
|
* An error captured from Android's C layer
|
||||||
*/
|
*/
|
||||||
C("c");
|
C("c"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error captured from a Dart / Flutter application
|
||||||
|
*/
|
||||||
|
DART("dart");
|
||||||
|
|
||||||
internal companion object {
|
internal companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@JvmName("fromDescriptor")
|
||||||
internal fun fromDescriptor(desc: String) = values().find { it.desc == desc }
|
internal fun fromDescriptor(desc: String) = values().find { it.desc == desc }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,8 @@ internal data class EventFilenameInfo(
|
||||||
val errorTypes: Set<ErrorType>
|
val errorTypes: Set<ErrorType>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a filename for the Event in the format
|
|
||||||
* "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json"
|
|
||||||
*/
|
|
||||||
fun encode(): String {
|
fun encode(): String {
|
||||||
return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json"
|
return toFilename(apiKey, uuid, timestamp, suffix, errorTypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH
|
fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH
|
||||||
|
@ -36,7 +32,21 @@ internal data class EventFilenameInfo(
|
||||||
private const val STARTUP_CRASH = "startupcrash"
|
private const val STARTUP_CRASH = "startupcrash"
|
||||||
private const val NON_JVM_CRASH = "not-jvm"
|
private const val NON_JVM_CRASH = "not-jvm"
|
||||||
|
|
||||||
@JvmOverloads
|
/**
|
||||||
|
* Generates a filename for the Event in the format
|
||||||
|
* "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json"
|
||||||
|
*/
|
||||||
|
fun toFilename(
|
||||||
|
apiKey: String,
|
||||||
|
uuid: String,
|
||||||
|
timestamp: Long,
|
||||||
|
suffix: String,
|
||||||
|
errorTypes: Set<ErrorType>
|
||||||
|
): String {
|
||||||
|
return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads @JvmStatic
|
||||||
fun fromEvent(
|
fun fromEvent(
|
||||||
obj: Any,
|
obj: Any,
|
||||||
uuid: String = UUID.randomUUID().toString(),
|
uuid: String = UUID.randomUUID().toString(),
|
||||||
|
@ -63,11 +73,12 @@ internal data class EventFilenameInfo(
|
||||||
/**
|
/**
|
||||||
* Reads event information from a filename.
|
* Reads event information from a filename.
|
||||||
*/
|
*/
|
||||||
|
@JvmStatic
|
||||||
fun fromFile(file: File, config: ImmutableConfig): EventFilenameInfo {
|
fun fromFile(file: File, config: ImmutableConfig): EventFilenameInfo {
|
||||||
return EventFilenameInfo(
|
return EventFilenameInfo(
|
||||||
findApiKeyInFilename(file, config),
|
findApiKeyInFilename(file, config),
|
||||||
"", // ignore UUID field when reading from file as unused
|
"", // ignore UUID field when reading from file as unused
|
||||||
-1, // ignore timestamp when reading from file as unused
|
findTimestampInFilename(file),
|
||||||
findSuffixInFilename(file),
|
findSuffixInFilename(file),
|
||||||
findErrorTypesInFilename(file)
|
findErrorTypesInFilename(file)
|
||||||
)
|
)
|
||||||
|
@ -77,7 +88,7 @@ internal data class EventFilenameInfo(
|
||||||
* Retrieves the api key encoded in the filename, or an empty string if this information
|
* Retrieves the api key encoded in the filename, or an empty string if this information
|
||||||
* is not encoded for the given event
|
* is not encoded for the given event
|
||||||
*/
|
*/
|
||||||
private fun findApiKeyInFilename(file: File, config: ImmutableConfig): String {
|
internal fun findApiKeyInFilename(file: File, config: ImmutableConfig): String {
|
||||||
val name = file.name.removeSuffix("_$STARTUP_CRASH.json")
|
val name = file.name.removeSuffix("_$STARTUP_CRASH.json")
|
||||||
val start = name.indexOf("_") + 1
|
val start = name.indexOf("_") + 1
|
||||||
val end = name.indexOf("_", start)
|
val end = name.indexOf("_", start)
|
||||||
|
@ -93,7 +104,7 @@ internal data class EventFilenameInfo(
|
||||||
* Retrieves the error types encoded in the filename, or an empty string if this
|
* Retrieves the error types encoded in the filename, or an empty string if this
|
||||||
* information is not encoded for the given event
|
* information is not encoded for the given event
|
||||||
*/
|
*/
|
||||||
private fun findErrorTypesInFilename(eventFile: File): Set<ErrorType> {
|
internal fun findErrorTypesInFilename(eventFile: File): Set<ErrorType> {
|
||||||
val name = eventFile.name
|
val name = eventFile.name
|
||||||
val end = name.lastIndexOf("_", name.lastIndexOf("_") - 1)
|
val end = name.lastIndexOf("_", name.lastIndexOf("_") - 1)
|
||||||
val start = name.lastIndexOf("_", end - 1) + 1
|
val start = name.lastIndexOf("_", end - 1) + 1
|
||||||
|
@ -111,7 +122,7 @@ internal data class EventFilenameInfo(
|
||||||
* Retrieves the error types encoded in the filename, or an empty string if this
|
* Retrieves the error types encoded in the filename, or an empty string if this
|
||||||
* information is not encoded for the given event
|
* information is not encoded for the given event
|
||||||
*/
|
*/
|
||||||
private fun findSuffixInFilename(eventFile: File): String {
|
internal fun findSuffixInFilename(eventFile: File): String {
|
||||||
val name = eventFile.nameWithoutExtension
|
val name = eventFile.nameWithoutExtension
|
||||||
val suffix = name.substring(name.lastIndexOf("_") + 1)
|
val suffix = name.substring(name.lastIndexOf("_") + 1)
|
||||||
return when (suffix) {
|
return when (suffix) {
|
||||||
|
@ -120,10 +131,20 @@ internal data class EventFilenameInfo(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the error types encoded in the filename, or an empty string if this
|
||||||
|
* information is not encoded for the given event
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun findTimestampInFilename(eventFile: File): Long {
|
||||||
|
val name = eventFile.nameWithoutExtension
|
||||||
|
return name.substringBefore("_", missingDelimiterValue = "-1").toLongOrNull() ?: -1
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the error types for the given event
|
* Retrieves the error types for the given event
|
||||||
*/
|
*/
|
||||||
private fun findErrorTypesForEvent(obj: Any): Set<ErrorType> {
|
internal fun findErrorTypesForEvent(obj: Any): Set<ErrorType> {
|
||||||
return when (obj) {
|
return when (obj) {
|
||||||
is Event -> obj.impl.getErrorTypesFromStackframes()
|
is Event -> obj.impl.getErrorTypesFromStackframes()
|
||||||
else -> setOf(ErrorType.C)
|
else -> setOf(ErrorType.C)
|
||||||
|
@ -133,7 +154,7 @@ internal data class EventFilenameInfo(
|
||||||
/**
|
/**
|
||||||
* Calculates the suffix for the given event
|
* Calculates the suffix for the given event
|
||||||
*/
|
*/
|
||||||
private fun findSuffixForEvent(obj: Any, launching: Boolean?): String {
|
internal fun findSuffixForEvent(obj: Any, launching: Boolean?): String {
|
||||||
return when {
|
return when {
|
||||||
obj is Event && obj.app.isLaunching == true -> STARTUP_CRASH
|
obj is Event && obj.app.isLaunching == true -> STARTUP_CRASH
|
||||||
launching == true -> STARTUP_CRASH
|
launching == true -> STARTUP_CRASH
|
||||||
|
|
|
@ -22,17 +22,18 @@ internal class EventStorageModule(
|
||||||
private val cfg = configModule.config
|
private val cfg = configModule.config
|
||||||
|
|
||||||
private val delegate by future {
|
private val delegate by future {
|
||||||
InternalReportDelegate(
|
if (cfg.telemetry.contains(Telemetry.INTERNAL_ERRORS) == true)
|
||||||
contextModule.ctx,
|
InternalReportDelegate(
|
||||||
cfg.logger,
|
contextModule.ctx,
|
||||||
cfg,
|
cfg.logger,
|
||||||
systemServiceModule.storageManager,
|
cfg,
|
||||||
dataCollectionModule.appDataCollector,
|
systemServiceModule.storageManager,
|
||||||
dataCollectionModule.deviceDataCollector,
|
dataCollectionModule.appDataCollector,
|
||||||
trackerModule.sessionTracker,
|
dataCollectionModule.deviceDataCollector,
|
||||||
notifier,
|
trackerModule.sessionTracker,
|
||||||
bgTaskService
|
notifier,
|
||||||
)
|
bgTaskService
|
||||||
|
) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
val eventStore by future { EventStore(cfg, cfg.logger, notifier, bgTaskService, delegate, callbackState) }
|
val eventStore by future { EventStore(cfg, cfg.logger, notifier, bgTaskService, delegate, callbackState) }
|
||||||
|
|
|
@ -7,9 +7,11 @@ import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
@ -119,7 +121,7 @@ class EventStore extends FileStore {
|
||||||
List<File> launchCrashes = new ArrayList<>();
|
List<File> launchCrashes = new ArrayList<>();
|
||||||
|
|
||||||
for (File file : storedFiles) {
|
for (File file : storedFiles) {
|
||||||
EventFilenameInfo filenameInfo = EventFilenameInfo.Companion.fromFile(file, config);
|
EventFilenameInfo filenameInfo = EventFilenameInfo.fromFile(file, config);
|
||||||
if (filenameInfo.isLaunchCrashReport()) {
|
if (filenameInfo.isLaunchCrashReport()) {
|
||||||
launchCrashes.add(file);
|
launchCrashes.add(file);
|
||||||
}
|
}
|
||||||
|
@ -163,7 +165,7 @@ class EventStore extends FileStore {
|
||||||
|
|
||||||
private void flushEventFile(File eventFile) {
|
private void flushEventFile(File eventFile) {
|
||||||
try {
|
try {
|
||||||
EventFilenameInfo eventInfo = EventFilenameInfo.Companion.fromFile(eventFile, config);
|
EventFilenameInfo eventInfo = EventFilenameInfo.fromFile(eventFile, config);
|
||||||
String apiKey = eventInfo.getApiKey();
|
String apiKey = eventInfo.getApiKey();
|
||||||
EventPayload payload = createEventPayload(eventFile, apiKey);
|
EventPayload payload = createEventPayload(eventFile, apiKey);
|
||||||
|
|
||||||
|
@ -188,9 +190,21 @@ class EventStore extends FileStore {
|
||||||
logger.i("Deleting sent error file " + eventFile.getName());
|
logger.i("Deleting sent error file " + eventFile.getName());
|
||||||
break;
|
break;
|
||||||
case UNDELIVERED:
|
case UNDELIVERED:
|
||||||
cancelQueuedFiles(Collections.singleton(eventFile));
|
if (isTooBig(eventFile)) {
|
||||||
logger.w("Could not send previously saved error(s)"
|
logger.w("Discarding over-sized event ("
|
||||||
+ " to Bugsnag, will try again later");
|
+ eventFile.length()
|
||||||
|
+ ") after failed delivery");
|
||||||
|
deleteStoredFiles(Collections.singleton(eventFile));
|
||||||
|
} else if (isTooOld(eventFile)) {
|
||||||
|
logger.w("Discarding historical event (from "
|
||||||
|
+ getCreationDate(eventFile)
|
||||||
|
+ ") after failed delivery");
|
||||||
|
deleteStoredFiles(Collections.singleton(eventFile));
|
||||||
|
} else {
|
||||||
|
cancelQueuedFiles(Collections.singleton(eventFile));
|
||||||
|
logger.w("Could not send previously saved error(s)"
|
||||||
|
+ " to Bugsnag, will try again later");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case FAILURE:
|
case FAILURE:
|
||||||
Exception exc = new RuntimeException("Failed to deliver event payload");
|
Exception exc = new RuntimeException("Failed to deliver event payload");
|
||||||
|
@ -234,13 +248,29 @@ class EventStore extends FileStore {
|
||||||
@Override
|
@Override
|
||||||
String getFilename(Object object) {
|
String getFilename(Object object) {
|
||||||
EventFilenameInfo eventInfo
|
EventFilenameInfo eventInfo
|
||||||
= EventFilenameInfo.Companion.fromEvent(object, null, config);
|
= EventFilenameInfo.fromEvent(object, null, config);
|
||||||
return eventInfo.encode();
|
return eventInfo.encode();
|
||||||
}
|
}
|
||||||
|
|
||||||
String getNdkFilename(Object object, String apiKey) {
|
String getNdkFilename(Object object, String apiKey) {
|
||||||
EventFilenameInfo eventInfo
|
EventFilenameInfo eventInfo
|
||||||
= EventFilenameInfo.Companion.fromEvent(object, apiKey, config);
|
= EventFilenameInfo.fromEvent(object, apiKey, config);
|
||||||
return eventInfo.encode();
|
return eventInfo.encode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long oneMegabyte = 1024 * 1024;
|
||||||
|
|
||||||
|
public boolean isTooBig(File file) {
|
||||||
|
return file.length() > oneMegabyte;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTooOld(File file) {
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.add(Calendar.DATE, -60);
|
||||||
|
return EventFilenameInfo.findTimestampInFilename(file) < cal.getTimeInMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getCreationDate(File file) {
|
||||||
|
return new Date(EventFilenameInfo.findTimestampInFilename(file));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ abstract class FileStore {
|
||||||
|
|
||||||
private final Lock lock = new ReentrantLock();
|
private final Lock lock = new ReentrantLock();
|
||||||
private final Collection<File> queuedFiles = new ConcurrentSkipListSet<>();
|
private final Collection<File> queuedFiles = new ConcurrentSkipListSet<>();
|
||||||
private final Logger logger;
|
protected final Logger logger;
|
||||||
private final EventStore.Delegate delegate;
|
private final EventStore.Delegate delegate;
|
||||||
|
|
||||||
FileStore(@NonNull File storageDir,
|
FileStore(@NonNull File storageDir,
|
||||||
|
|
|
@ -37,6 +37,7 @@ internal class ManifestConfigLoader {
|
||||||
private const val MAX_BREADCRUMBS = "$BUGSNAG_NS.MAX_BREADCRUMBS"
|
private const val MAX_BREADCRUMBS = "$BUGSNAG_NS.MAX_BREADCRUMBS"
|
||||||
private const val MAX_PERSISTED_EVENTS = "$BUGSNAG_NS.MAX_PERSISTED_EVENTS"
|
private const val MAX_PERSISTED_EVENTS = "$BUGSNAG_NS.MAX_PERSISTED_EVENTS"
|
||||||
private const val MAX_PERSISTED_SESSIONS = "$BUGSNAG_NS.MAX_PERSISTED_SESSIONS"
|
private const val MAX_PERSISTED_SESSIONS = "$BUGSNAG_NS.MAX_PERSISTED_SESSIONS"
|
||||||
|
private const val MAX_REPORTED_THREADS = "$BUGSNAG_NS.MAX_REPORTED_THREADS"
|
||||||
private const val LAUNCH_CRASH_THRESHOLD_MS = "$BUGSNAG_NS.LAUNCH_CRASH_THRESHOLD_MS"
|
private const val LAUNCH_CRASH_THRESHOLD_MS = "$BUGSNAG_NS.LAUNCH_CRASH_THRESHOLD_MS"
|
||||||
private const val LAUNCH_DURATION_MILLIS = "$BUGSNAG_NS.LAUNCH_DURATION_MILLIS"
|
private const val LAUNCH_DURATION_MILLIS = "$BUGSNAG_NS.LAUNCH_DURATION_MILLIS"
|
||||||
private const val SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY"
|
private const val SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY"
|
||||||
|
@ -77,6 +78,7 @@ internal class ManifestConfigLoader {
|
||||||
maxBreadcrumbs = data.getInt(MAX_BREADCRUMBS, maxBreadcrumbs)
|
maxBreadcrumbs = data.getInt(MAX_BREADCRUMBS, maxBreadcrumbs)
|
||||||
maxPersistedEvents = data.getInt(MAX_PERSISTED_EVENTS, maxPersistedEvents)
|
maxPersistedEvents = data.getInt(MAX_PERSISTED_EVENTS, maxPersistedEvents)
|
||||||
maxPersistedSessions = data.getInt(MAX_PERSISTED_SESSIONS, maxPersistedSessions)
|
maxPersistedSessions = data.getInt(MAX_PERSISTED_SESSIONS, maxPersistedSessions)
|
||||||
|
maxReportedThreads = data.getInt(MAX_REPORTED_THREADS, maxReportedThreads)
|
||||||
launchDurationMillis = data.getInt(
|
launchDurationMillis = data.getInt(
|
||||||
LAUNCH_CRASH_THRESHOLD_MS,
|
LAUNCH_CRASH_THRESHOLD_MS,
|
||||||
launchDurationMillis.toInt()
|
launchDurationMillis.toInt()
|
||||||
|
|
|
@ -45,7 +45,12 @@ class NativeStackframe internal constructor(
|
||||||
/**
|
/**
|
||||||
* The type of the error
|
* The type of the error
|
||||||
*/
|
*/
|
||||||
var type: ErrorType? = null
|
var type: ErrorType? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies the exact build this frame originates from.
|
||||||
|
*/
|
||||||
|
var codeIdentifier: String? = null,
|
||||||
) : JsonStream.Streamable {
|
) : JsonStream.Streamable {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
@ -57,6 +62,7 @@ class NativeStackframe internal constructor(
|
||||||
writer.name("frameAddress").value(frameAddress)
|
writer.name("frameAddress").value(frameAddress)
|
||||||
writer.name("symbolAddress").value(symbolAddress)
|
writer.name("symbolAddress").value(symbolAddress)
|
||||||
writer.name("loadAddress").value(loadAddress)
|
writer.name("loadAddress").value(loadAddress)
|
||||||
|
writer.name("codeIdentifier").value(codeIdentifier)
|
||||||
writer.name("isPC").value(isPC)
|
writer.name("isPC").value(isPC)
|
||||||
|
|
||||||
type?.let {
|
type?.let {
|
||||||
|
|
|
@ -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 = "5.19.2",
|
var version: String = "5.23.0",
|
||||||
var url: String = "https://bugsnag.com"
|
var url: String = "https://bugsnag.com"
|
||||||
) : JsonStream.Streamable {
|
) : JsonStream.Streamable {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents important information about a session filename.
|
||||||
|
* Currently the following information is encoded:
|
||||||
|
*
|
||||||
|
* uuid - to disambiguate stored error reports
|
||||||
|
* timestamp - to sort error reports by time of capture
|
||||||
|
*/
|
||||||
|
internal data class SessionFilenameInfo(
|
||||||
|
val timestamp: Long,
|
||||||
|
val uuid: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun encode(): String {
|
||||||
|
return toFilename(timestamp, uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal companion object {
|
||||||
|
|
||||||
|
const val uuidLength = 36
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a filename for the session in the format
|
||||||
|
* "[UUID][timestamp]_v2.json"
|
||||||
|
*/
|
||||||
|
fun toFilename(timestamp: Long, uuid: String): String {
|
||||||
|
return "${uuid}${timestamp}_v2.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun defaultFilename(): String {
|
||||||
|
return toFilename(System.currentTimeMillis(), UUID.randomUUID().toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromFile(file: File): SessionFilenameInfo {
|
||||||
|
return SessionFilenameInfo(
|
||||||
|
findTimestampInFilename(file),
|
||||||
|
findUuidInFilename(file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findUuidInFilename(file: File): String {
|
||||||
|
return file.name.substring(0, uuidLength - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun findTimestampInFilename(file: File): Long {
|
||||||
|
return file.name.substring(uuidLength, file.name.indexOf("_")).toLongOrNull() ?: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,17 +2,32 @@ package com.bugsnag.android
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
||||||
internal class SessionLifecycleCallback(
|
internal class SessionLifecycleCallback(
|
||||||
private val sessionTracker: SessionTracker
|
private val sessionTracker: SessionTracker
|
||||||
) : Application.ActivityLifecycleCallbacks {
|
) : Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
override fun onActivityStarted(activity: Activity) =
|
override fun onActivityStarted(activity: Activity) {
|
||||||
sessionTracker.onActivityStarted(activity.javaClass.simpleName)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
sessionTracker.onActivityStarted(activity.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityStopped(activity: Activity) =
|
override fun onActivityPostStarted(activity: Activity) {
|
||||||
|
sessionTracker.onActivityStarted(activity.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStopped(activity: Activity) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
sessionTracker.onActivityStopped(activity.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityPostStopped(activity: Activity) {
|
||||||
sessionTracker.onActivityStopped(activity.javaClass.simpleName)
|
sessionTracker.onActivityStopped(activity.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
||||||
override fun onActivityResumed(activity: Activity) {}
|
override fun onActivityResumed(activity: Activity) {}
|
||||||
|
|
|
@ -6,7 +6,9 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.Calendar;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,7 +48,16 @@ class SessionStore extends FileStore {
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
String getFilename(Object object) {
|
String getFilename(Object object) {
|
||||||
return UUID.randomUUID().toString() + System.currentTimeMillis() + "_v2.json";
|
return SessionFilenameInfo.defaultFilename();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isTooOld(File file) {
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.add(Calendar.DATE, -60);
|
||||||
|
return SessionFilenameInfo.findTimestampInFilename(file) < cal.getTimeInMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getCreationDate(File file) {
|
||||||
|
return new Date(SessionFilenameInfo.findTimestampInFilename(file));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,8 +270,15 @@ class SessionTracker extends BaseObservable {
|
||||||
logger.d("Sent 1 new session to Bugsnag");
|
logger.d("Sent 1 new session to Bugsnag");
|
||||||
break;
|
break;
|
||||||
case UNDELIVERED:
|
case UNDELIVERED:
|
||||||
sessionStore.cancelQueuedFiles(Collections.singletonList(storedFile));
|
if (sessionStore.isTooOld(storedFile)) {
|
||||||
logger.w("Leaving session payload for future delivery");
|
logger.w("Discarding historical session (from {"
|
||||||
|
+ sessionStore.getCreationDate(storedFile)
|
||||||
|
+ "}) after failed delivery");
|
||||||
|
sessionStore.deleteStoredFiles(Collections.singletonList(storedFile));
|
||||||
|
} else {
|
||||||
|
sessionStore.cancelQueuedFiles(Collections.singletonList(storedFile));
|
||||||
|
logger.w("Leaving session payload for future delivery");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case FAILURE:
|
case FAILURE:
|
||||||
// drop bad data
|
// drop bad data
|
||||||
|
|
|
@ -6,12 +6,15 @@ import android.content.Context
|
||||||
/**
|
/**
|
||||||
* Reads legacy information left in SharedPreferences and migrates it to the new location.
|
* Reads legacy information left in SharedPreferences and migrates it to the new location.
|
||||||
*/
|
*/
|
||||||
internal class SharedPrefMigrator(context: Context) {
|
internal class SharedPrefMigrator(context: Context) : DeviceIdPersistence {
|
||||||
|
|
||||||
private val prefs = context
|
private val prefs = context
|
||||||
.getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE)
|
.getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
fun loadDeviceId() = prefs.getString(INSTALL_ID_KEY, null)
|
/**
|
||||||
|
* This implementation will never create an ID; it will only fetch one if present.
|
||||||
|
*/
|
||||||
|
override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean) = prefs.getString(INSTALL_ID_KEY, null)
|
||||||
|
|
||||||
fun loadUser(deviceId: String?) = User(
|
fun loadUser(deviceId: String?) = User(
|
||||||
prefs.getString(USER_ID_KEY, deviceId),
|
prefs.getString(USER_ID_KEY, deviceId),
|
||||||
|
|
|
@ -53,6 +53,11 @@ class Stackframe : JsonStream.Streamable {
|
||||||
*/
|
*/
|
||||||
var loadAddress: Long? = null
|
var loadAddress: Long? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies the exact build this frame originates from.
|
||||||
|
*/
|
||||||
|
var codeIdentifier: String? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this frame identifies the program counter
|
* Whether this frame identifies the program counter
|
||||||
*/
|
*/
|
||||||
|
@ -90,6 +95,7 @@ class Stackframe : JsonStream.Streamable {
|
||||||
this.frameAddress = nativeFrame.frameAddress
|
this.frameAddress = nativeFrame.frameAddress
|
||||||
this.symbolAddress = nativeFrame.symbolAddress
|
this.symbolAddress = nativeFrame.symbolAddress
|
||||||
this.loadAddress = nativeFrame.loadAddress
|
this.loadAddress = nativeFrame.loadAddress
|
||||||
|
this.codeIdentifier = nativeFrame.codeIdentifier
|
||||||
this.isPC = nativeFrame.isPC
|
this.isPC = nativeFrame.isPC
|
||||||
this.type = nativeFrame.type
|
this.type = nativeFrame.type
|
||||||
}
|
}
|
||||||
|
@ -103,6 +109,7 @@ class Stackframe : JsonStream.Streamable {
|
||||||
frameAddress = (json["frameAddress"] as? Number)?.toLong()
|
frameAddress = (json["frameAddress"] as? Number)?.toLong()
|
||||||
symbolAddress = (json["symbolAddress"] as? Number)?.toLong()
|
symbolAddress = (json["symbolAddress"] as? Number)?.toLong()
|
||||||
loadAddress = (json["loadAddress"] as? Number)?.toLong()
|
loadAddress = (json["loadAddress"] as? Number)?.toLong()
|
||||||
|
codeIdentifier = (json["codeIdentifier"] as? String)
|
||||||
isPC = json["isPC"] as? Boolean
|
isPC = json["isPC"] as? Boolean
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@ -124,6 +131,7 @@ class Stackframe : JsonStream.Streamable {
|
||||||
frameAddress?.let { writer.name("frameAddress").value(it) }
|
frameAddress?.let { writer.name("frameAddress").value(it) }
|
||||||
symbolAddress?.let { writer.name("symbolAddress").value(it) }
|
symbolAddress?.let { writer.name("symbolAddress").value(it) }
|
||||||
loadAddress?.let { writer.name("loadAddress").value(it) }
|
loadAddress?.let { writer.name("loadAddress").value(it) }
|
||||||
|
codeIdentifier?.let { writer.name("codeIdentifier").value(it) }
|
||||||
isPC?.let { writer.name("isPC").value(it) }
|
isPC?.let { writer.name("isPC").value(it) }
|
||||||
|
|
||||||
type?.let {
|
type?.let {
|
||||||
|
|
|
@ -25,6 +25,8 @@ internal class StorageModule(
|
||||||
|
|
||||||
val deviceId by future { deviceIdStore.loadDeviceId() }
|
val deviceId by future { deviceIdStore.loadDeviceId() }
|
||||||
|
|
||||||
|
val internalDeviceId by future { deviceIdStore.loadInternalDeviceId() }
|
||||||
|
|
||||||
val userStore by future {
|
val userStore by future {
|
||||||
UserStore(
|
UserStore(
|
||||||
immutableConfig,
|
immutableConfig,
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of telemetry that may be sent to Bugsnag for product improvement purposes.
|
||||||
|
*/
|
||||||
|
enum class Telemetry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Errors within the Bugsnag SDK.
|
||||||
|
*/
|
||||||
|
INTERNAL_ERRORS;
|
||||||
|
|
||||||
|
internal companion object {
|
||||||
|
fun fromString(str: String) = values().find { it.name == str } ?: INTERNAL_ERRORS
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,25 +2,27 @@ package com.bugsnag.android
|
||||||
|
|
||||||
import com.bugsnag.android.internal.ImmutableConfig
|
import com.bugsnag.android.internal.ImmutableConfig
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.lang.Thread as JavaThread
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture and serialize the state of all threads at the time of an exception.
|
* Capture and serialize the state of all threads at the time of an exception.
|
||||||
*/
|
*/
|
||||||
internal class ThreadState @Suppress("LongParameterList") @JvmOverloads constructor(
|
internal class ThreadState @Suppress("LongParameterList") constructor(
|
||||||
exc: Throwable?,
|
exc: Throwable?,
|
||||||
isUnhandled: Boolean,
|
isUnhandled: Boolean,
|
||||||
|
maxThreads: Int,
|
||||||
sendThreads: ThreadSendPolicy,
|
sendThreads: ThreadSendPolicy,
|
||||||
projectPackages: Collection<String>,
|
projectPackages: Collection<String>,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
currentThread: java.lang.Thread? = null,
|
currentThread: JavaThread = JavaThread.currentThread(),
|
||||||
stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>? = null
|
allThreads: List<JavaThread> = allThreads()
|
||||||
) : JsonStream.Streamable {
|
) : JsonStream.Streamable {
|
||||||
|
|
||||||
internal constructor(
|
internal constructor(
|
||||||
exc: Throwable?,
|
exc: Throwable?,
|
||||||
isUnhandled: Boolean,
|
isUnhandled: Boolean,
|
||||||
config: ImmutableConfig
|
config: ImmutableConfig
|
||||||
) : this(exc, isUnhandled, config.sendThreads, config.projectPackages, config.logger)
|
) : this(exc, isUnhandled, config.maxReportedThreads, config.sendThreads, config.projectPackages, config.logger)
|
||||||
|
|
||||||
val threads: MutableList<Thread>
|
val threads: MutableList<Thread>
|
||||||
|
|
||||||
|
@ -30,10 +32,11 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
|
||||||
|
|
||||||
threads = when {
|
threads = when {
|
||||||
recordThreads -> captureThreadTrace(
|
recordThreads -> captureThreadTrace(
|
||||||
stackTraces ?: java.lang.Thread.getAllStackTraces(),
|
allThreads,
|
||||||
currentThread ?: java.lang.Thread.currentThread(),
|
currentThread,
|
||||||
exc,
|
exc,
|
||||||
isUnhandled,
|
isUnhandled,
|
||||||
|
maxThreads,
|
||||||
projectPackages,
|
projectPackages,
|
||||||
logger
|
logger
|
||||||
)
|
)
|
||||||
|
@ -41,37 +44,88 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun rootThreadGroup(): ThreadGroup {
|
||||||
|
var group = JavaThread.currentThread().threadGroup!!
|
||||||
|
|
||||||
|
while (group.parent != null) {
|
||||||
|
group = group.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun allThreads(): List<JavaThread> {
|
||||||
|
val rootGroup = rootThreadGroup()
|
||||||
|
val threadCount = rootGroup.activeCount()
|
||||||
|
val threads: Array<JavaThread?> = arrayOfNulls(threadCount)
|
||||||
|
rootGroup.enumerate(threads)
|
||||||
|
return threads.filterNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun captureThreadTrace(
|
private fun captureThreadTrace(
|
||||||
stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>,
|
allThreads: List<JavaThread>,
|
||||||
currentThread: java.lang.Thread,
|
currentThread: JavaThread,
|
||||||
exc: Throwable?,
|
exc: Throwable?,
|
||||||
isUnhandled: Boolean,
|
isUnhandled: Boolean,
|
||||||
|
maxThreadCount: Int,
|
||||||
projectPackages: Collection<String>,
|
projectPackages: Collection<String>,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
): MutableList<Thread> {
|
): MutableList<Thread> {
|
||||||
// API 24/25 don't record the currentThread, add it in manually
|
fun toBugsnagThread(thread: JavaThread): Thread {
|
||||||
// https://issuetracker.google.com/issues/64122757
|
val isErrorThread = thread.id == currentThread.id
|
||||||
if (!stackTraces.containsKey(currentThread)) {
|
val stackTrace = Stacktrace(
|
||||||
stackTraces[currentThread] = currentThread.stackTrace
|
if (isErrorThread) {
|
||||||
}
|
if (exc != null && isUnhandled) { // unhandled errors use the exception trace for thread traces
|
||||||
if (exc != null && isUnhandled) { // unhandled errors use the exception trace for thread traces
|
exc.stackTrace
|
||||||
stackTraces[currentThread] = exc.stackTrace
|
} else {
|
||||||
}
|
currentThread.stackTrace
|
||||||
|
}
|
||||||
val currentThreadId = currentThread.id
|
|
||||||
return stackTraces.keys
|
|
||||||
.sortedBy { it.id }
|
|
||||||
.mapNotNull { thread ->
|
|
||||||
val trace = stackTraces[thread]
|
|
||||||
|
|
||||||
if (trace != null) {
|
|
||||||
val stacktrace = Stacktrace(trace, projectPackages, logger)
|
|
||||||
val errorThread = thread.id == currentThreadId
|
|
||||||
Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, Thread.State.forThread(thread), stacktrace, logger)
|
|
||||||
} else {
|
} else {
|
||||||
null
|
thread.stackTrace
|
||||||
}
|
},
|
||||||
}.toMutableList()
|
projectPackages, logger
|
||||||
|
)
|
||||||
|
|
||||||
|
return Thread(
|
||||||
|
thread.id,
|
||||||
|
thread.name,
|
||||||
|
ThreadType.ANDROID,
|
||||||
|
isErrorThread,
|
||||||
|
Thread.State.forThread(thread),
|
||||||
|
stackTrace,
|
||||||
|
logger
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the lowest ID threads (ordered). Anything after maxThreadCount is lost.
|
||||||
|
// Note: We must ensure that currentThread is always present in the final list regardless.
|
||||||
|
val keepThreads = allThreads.sortedBy { it.id }.take(maxThreadCount)
|
||||||
|
|
||||||
|
val reportThreads = if (keepThreads.contains(currentThread)) {
|
||||||
|
keepThreads
|
||||||
|
} else {
|
||||||
|
// API 24/25 don't record the currentThread, so add it in manually
|
||||||
|
// https://issuetracker.google.com/issues/64122757
|
||||||
|
// currentThread may also have been removed if its ID occurred after maxThreadCount
|
||||||
|
keepThreads.take(Math.max(maxThreadCount - 1, 0)).plus(currentThread).sortedBy { it.id }
|
||||||
|
}.map { toBugsnagThread(it) }.toMutableList()
|
||||||
|
|
||||||
|
if (allThreads.size > maxThreadCount) {
|
||||||
|
reportThreads.add(
|
||||||
|
Thread(
|
||||||
|
-1,
|
||||||
|
"[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]",
|
||||||
|
ThreadType.EMPTY,
|
||||||
|
false,
|
||||||
|
Thread.State.UNKNOWN,
|
||||||
|
Stacktrace(arrayOf(StackTraceElement("", "", "-", 0)), projectPackages, logger),
|
||||||
|
logger
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return reportThreads
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
|
|
@ -5,6 +5,11 @@ package com.bugsnag.android
|
||||||
*/
|
*/
|
||||||
enum class ThreadType(internal val desc: String) {
|
enum class ThreadType(internal val desc: String) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A thread captured from Android's JVM layer
|
||||||
|
*/
|
||||||
|
EMPTY(""),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A thread captured from Android's JVM layer
|
* A thread captured from Android's JVM layer
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
import com.bugsnag.android.BugsnagEventMapper
|
||||||
|
import com.bugsnag.android.Event
|
||||||
|
import com.bugsnag.android.JsonStream
|
||||||
|
import com.bugsnag.android.Logger
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import com.bugsnag.android.Error as BugsnagError
|
||||||
|
|
||||||
|
class BugsnagMapper(logger: Logger) {
|
||||||
|
private val eventMapper = BugsnagEventMapper(logger)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given `Map` of data to an `Event` object
|
||||||
|
*/
|
||||||
|
fun convertToEvent(data: Map<in String, Any?>, fallbackApiKey: String): Event {
|
||||||
|
return eventMapper.convertToEvent(data, fallbackApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given `Map` of data to an `Error` object
|
||||||
|
*/
|
||||||
|
fun convertToError(data: Map<in String, Any?>): BugsnagError {
|
||||||
|
return eventMapper.convertError(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a given `Event` object to a `Map<String, Any?>`
|
||||||
|
*/
|
||||||
|
fun convertToMap(event: Event): Map<in String, Any?> {
|
||||||
|
val byteStream = ByteArrayOutputStream()
|
||||||
|
byteStream.writer().use { writer -> JsonStream(writer).value(event) }
|
||||||
|
return JsonHelper.deserialize(byteStream.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a given `Error` object to a `Map<String, Any?>`
|
||||||
|
*/
|
||||||
|
fun convertToMap(error: BugsnagError): Map<in String, Any?> {
|
||||||
|
val byteStream = ByteArrayOutputStream()
|
||||||
|
byteStream.writer().use { writer -> JsonStream(writer).value(error) }
|
||||||
|
return JsonHelper.deserialize(byteStream.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import com.bugsnag.android.EventPayload
|
||||||
import com.bugsnag.android.Logger
|
import com.bugsnag.android.Logger
|
||||||
import com.bugsnag.android.ManifestConfigLoader.Companion.BUILD_UUID
|
import com.bugsnag.android.ManifestConfigLoader.Companion.BUILD_UUID
|
||||||
import com.bugsnag.android.NoopLogger
|
import com.bugsnag.android.NoopLogger
|
||||||
|
import com.bugsnag.android.Telemetry
|
||||||
import com.bugsnag.android.ThreadSendPolicy
|
import com.bugsnag.android.ThreadSendPolicy
|
||||||
import com.bugsnag.android.errorApiHeaders
|
import com.bugsnag.android.errorApiHeaders
|
||||||
import com.bugsnag.android.safeUnrollCauses
|
import com.bugsnag.android.safeUnrollCauses
|
||||||
|
@ -34,6 +35,7 @@ data class ImmutableConfig(
|
||||||
val enabledReleaseStages: Collection<String>?,
|
val enabledReleaseStages: Collection<String>?,
|
||||||
val projectPackages: Collection<String>,
|
val projectPackages: Collection<String>,
|
||||||
val enabledBreadcrumbTypes: Set<BreadcrumbType>?,
|
val enabledBreadcrumbTypes: Set<BreadcrumbType>?,
|
||||||
|
val telemetry: Set<Telemetry>,
|
||||||
val releaseStage: String?,
|
val releaseStage: String?,
|
||||||
val buildUuid: String?,
|
val buildUuid: String?,
|
||||||
val appVersion: String?,
|
val appVersion: String?,
|
||||||
|
@ -47,6 +49,7 @@ data class ImmutableConfig(
|
||||||
val maxBreadcrumbs: Int,
|
val maxBreadcrumbs: Int,
|
||||||
val maxPersistedEvents: Int,
|
val maxPersistedEvents: Int,
|
||||||
val maxPersistedSessions: Int,
|
val maxPersistedSessions: Int,
|
||||||
|
val maxReportedThreads: Int,
|
||||||
val persistenceDirectory: Lazy<File>,
|
val persistenceDirectory: Lazy<File>,
|
||||||
val sendLaunchCrashesSynchronously: Boolean,
|
val sendLaunchCrashesSynchronously: Boolean,
|
||||||
|
|
||||||
|
@ -159,7 +162,9 @@ internal fun convertToImmutableConfig(
|
||||||
maxBreadcrumbs = config.maxBreadcrumbs,
|
maxBreadcrumbs = config.maxBreadcrumbs,
|
||||||
maxPersistedEvents = config.maxPersistedEvents,
|
maxPersistedEvents = config.maxPersistedEvents,
|
||||||
maxPersistedSessions = config.maxPersistedSessions,
|
maxPersistedSessions = config.maxPersistedSessions,
|
||||||
|
maxReportedThreads = config.maxReportedThreads,
|
||||||
enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(),
|
enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(),
|
||||||
|
telemetry = config.telemetry.toSet(),
|
||||||
persistenceDirectory = persistenceDir,
|
persistenceDirectory = persistenceDir,
|
||||||
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously,
|
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously,
|
||||||
packageInfo = packageInfo,
|
packageInfo = packageInfo,
|
||||||
|
|
|
@ -92,6 +92,7 @@ import com.bugsnag.android.OnErrorCallback;
|
||||||
import com.bugsnag.android.OnSessionCallback;
|
import com.bugsnag.android.OnSessionCallback;
|
||||||
import com.bugsnag.android.Session;
|
import com.bugsnag.android.Session;
|
||||||
import com.bugsnag.android.Severity;
|
import com.bugsnag.android.Severity;
|
||||||
|
import com.bugsnag.android.Telemetry;
|
||||||
import com.sun.mail.iap.BadCommandException;
|
import com.sun.mail.iap.BadCommandException;
|
||||||
import com.sun.mail.iap.ConnectionException;
|
import com.sun.mail.iap.ConnectionException;
|
||||||
import com.sun.mail.iap.ProtocolException;
|
import com.sun.mail.iap.ProtocolException;
|
||||||
|
@ -370,6 +371,7 @@ public class Log {
|
||||||
// https://docs.bugsnag.com/platforms/android/sdk/
|
// https://docs.bugsnag.com/platforms/android/sdk/
|
||||||
com.bugsnag.android.Configuration config =
|
com.bugsnag.android.Configuration config =
|
||||||
new com.bugsnag.android.Configuration("9d2d57476a0614974449a3ec33f2604a");
|
new com.bugsnag.android.Configuration("9d2d57476a0614974449a3ec33f2604a");
|
||||||
|
config.setTelemetry(Collections.emptySet());
|
||||||
|
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
config.setReleaseStage("debug");
|
config.setReleaseStage("debug");
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
||||||
index 0ce2eec8c..e1bac196e 100644
|
index 0ce2eec8c4..e1bac196e2 100644
|
||||||
--- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
--- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
||||||
+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
||||||
@@ -66,7 +66,7 @@ internal class DefaultDelivery(
|
@@ -66,7 +66,7 @@ internal class DefaultDelivery(
|
||||||
|
@ -12,24 +12,15 @@ index 0ce2eec8c..e1bac196e 100644
|
||||||
logger.w("Unexpected error delivering payload", exception)
|
logger.w("Unexpected error delivering payload", exception)
|
||||||
return DeliveryStatus.FAILURE
|
return DeliveryStatus.FAILURE
|
||||||
diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch
|
diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch
|
||||||
index 25c19fd4c..e69de29bb 100644
|
index c762d488a1..e69de29bb2 100644
|
||||||
--- a/patches/Bugsnag.patch
|
--- a/patches/Bugsnag.patch
|
||||||
+++ b/patches/Bugsnag.patch
|
+++ b/patches/Bugsnag.patch
|
||||||
@@ -1,22 +0,0 @@
|
@@ -1,40 +0,0 @@
|
||||||
-From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001
|
|
||||||
-From: M66B <M66B@users.noreply.github.com>
|
|
||||||
-Date: Sat, 15 May 2021 22:07:24 +0200
|
|
||||||
-Subject: [PATCH] Bugsnag failure on I/O error
|
|
||||||
-
|
|
||||||
----
|
|
||||||
- app/src/main/java/com/bugsnag/android/DefaultDelivery.kt | 2 +-
|
|
||||||
- 1 file changed, 1 insertion(+), 1 deletion(-)
|
|
||||||
-
|
|
||||||
-diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
-diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
||||||
-index a7995164cb4e..5620f0bacd80 100644
|
-index 0ce2eec8c..e1bac196e 100644
|
||||||
---- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
---- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
||||||
-+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
-+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
||||||
-@@ -64,7 +64,7 @@ internal class DefaultDelivery(
|
-@@ -66,7 +66,7 @@ internal class DefaultDelivery(
|
||||||
- return DeliveryStatus.UNDELIVERED
|
- return DeliveryStatus.UNDELIVERED
|
||||||
- } catch (exception: IOException) {
|
- } catch (exception: IOException) {
|
||||||
- logger.w("IOException encountered in request", exception)
|
- logger.w("IOException encountered in request", exception)
|
||||||
|
@ -38,3 +29,30 @@ index 25c19fd4c..e69de29bb 100644
|
||||||
- } catch (exception: Exception) {
|
- } catch (exception: Exception) {
|
||||||
- logger.w("Unexpected error delivering payload", exception)
|
- logger.w("Unexpected error delivering payload", exception)
|
||||||
- return DeliveryStatus.FAILURE
|
- return DeliveryStatus.FAILURE
|
||||||
|
-diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch
|
||||||
|
-index 25c19fd4c..e69de29bb 100644
|
||||||
|
---- a/patches/Bugsnag.patch
|
||||||
|
-+++ b/patches/Bugsnag.patch
|
||||||
|
-@@ -1,22 +0,0 @@
|
||||||
|
--From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001
|
||||||
|
--From: M66B <M66B@users.noreply.github.com>
|
||||||
|
--Date: Sat, 15 May 2021 22:07:24 +0200
|
||||||
|
--Subject: [PATCH] Bugsnag failure on I/O error
|
||||||
|
--
|
||||||
|
-----
|
||||||
|
-- app/src/main/java/com/bugsnag/android/DefaultDelivery.kt | 2 +-
|
||||||
|
-- 1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
--
|
||||||
|
--diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
||||||
|
--index a7995164cb4e..5620f0bacd80 100644
|
||||||
|
----- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
||||||
|
--+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
|
||||||
|
--@@ -64,7 +64,7 @@ internal class DefaultDelivery(
|
||||||
|
-- return DeliveryStatus.UNDELIVERED
|
||||||
|
-- } catch (exception: IOException) {
|
||||||
|
-- logger.w("IOException encountered in request", exception)
|
||||||
|
--- return DeliveryStatus.UNDELIVERED
|
||||||
|
--+ return DeliveryStatus.FAILURE
|
||||||
|
-- } catch (exception: Exception) {
|
||||||
|
-- logger.w("Unexpected error delivering payload", exception)
|
||||||
|
-- return DeliveryStatus.FAILURE
|
||||||
|
|
Loading…
Reference in New Issue