mirror of https://github.com/M66B/FairEmail.git
1161 lines
42 KiB
Java
1161 lines
42 KiB
Java
package com.bugsnag.android;
|
|
|
|
import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION;
|
|
|
|
import com.bugsnag.android.internal.ImmutableConfig;
|
|
import com.bugsnag.android.internal.StateObserver;
|
|
import com.bugsnag.android.internal.dag.ConfigModule;
|
|
import com.bugsnag.android.internal.dag.ContextModule;
|
|
import com.bugsnag.android.internal.dag.SystemServiceModule;
|
|
|
|
import android.app.Application;
|
|
import android.content.Context;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
import kotlin.Unit;
|
|
import kotlin.jvm.functions.Function1;
|
|
import kotlin.jvm.functions.Function2;
|
|
|
|
import java.io.File;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.Callable;
|
|
import java.util.concurrent.RejectedExecutionException;
|
|
|
|
/**
|
|
* A Bugsnag Client instance allows you to use Bugsnag in your Android app.
|
|
* Typically you'd instead use the static access provided in the Bugsnag class.
|
|
* <p/>
|
|
* Example usage:
|
|
* <p/>
|
|
* Client client = new Client(this, "your-api-key");
|
|
* client.notify(new RuntimeException("something broke!"));
|
|
*
|
|
* @see Bugsnag
|
|
*/
|
|
@SuppressWarnings({"checkstyle:JavadocTagContinuationIndentation", "ConstantConditions"})
|
|
public class Client implements MetadataAware, CallbackAware, UserAware, FeatureFlagAware {
|
|
|
|
final ImmutableConfig immutableConfig;
|
|
|
|
final MetadataState metadataState;
|
|
final FeatureFlagState featureFlagState;
|
|
|
|
private final ContextState contextState;
|
|
private final CallbackState callbackState;
|
|
private final UserState userState;
|
|
|
|
final Context appContext;
|
|
|
|
@NonNull
|
|
final DeviceDataCollector deviceDataCollector;
|
|
|
|
@NonNull
|
|
final AppDataCollector appDataCollector;
|
|
|
|
@NonNull
|
|
final BreadcrumbState breadcrumbState;
|
|
|
|
@NonNull
|
|
final MemoryTrimState memoryTrimState = new MemoryTrimState();
|
|
|
|
@NonNull
|
|
protected final EventStore eventStore;
|
|
|
|
final SessionTracker sessionTracker;
|
|
|
|
final SystemBroadcastReceiver systemBroadcastReceiver;
|
|
|
|
final Logger logger;
|
|
final Connectivity connectivity;
|
|
final DeliveryDelegate deliveryDelegate;
|
|
|
|
final ClientObservable clientObservable;
|
|
PluginClient pluginClient;
|
|
|
|
final Notifier notifier;
|
|
|
|
@Nullable
|
|
final LastRunInfo lastRunInfo;
|
|
final LastRunInfoStore lastRunInfoStore;
|
|
final LaunchCrashTracker launchCrashTracker;
|
|
final BackgroundTaskService bgTaskService = new BackgroundTaskService();
|
|
private final ExceptionHandler exceptionHandler;
|
|
|
|
/**
|
|
* Initialize a Bugsnag client
|
|
*
|
|
* @param androidContext an Android context, usually <code>this</code>
|
|
*/
|
|
public Client(@NonNull Context androidContext) {
|
|
this(androidContext, Configuration.load(androidContext));
|
|
}
|
|
|
|
/**
|
|
* Initialize a Bugsnag client
|
|
*
|
|
* @param androidContext an Android context, usually <code>this</code>
|
|
* @param apiKey your Bugsnag API key from your Bugsnag dashboard
|
|
*/
|
|
public Client(@NonNull Context androidContext, @NonNull String apiKey) {
|
|
this(androidContext, Configuration.load(androidContext, apiKey));
|
|
}
|
|
|
|
/**
|
|
* Initialize a Bugsnag client
|
|
*
|
|
* @param androidContext an Android context, usually <code>this</code>
|
|
* @param configuration a configuration for the Client
|
|
*/
|
|
public Client(@NonNull Context androidContext, @NonNull final Configuration configuration) {
|
|
ContextModule contextModule = new ContextModule(androidContext);
|
|
appContext = contextModule.getCtx();
|
|
|
|
notifier = configuration.getNotifier();
|
|
|
|
connectivity = new ConnectivityCompat(appContext, new Function2<Boolean, String, Unit>() {
|
|
@Override
|
|
public Unit invoke(Boolean hasConnection, String networkState) {
|
|
Map<String, Object> data = new HashMap<>();
|
|
data.put("hasConnection", hasConnection);
|
|
data.put("networkState", networkState);
|
|
leaveAutoBreadcrumb("Connectivity changed", BreadcrumbType.STATE, data);
|
|
if (hasConnection) {
|
|
eventStore.flushAsync();
|
|
sessionTracker.flushAsync();
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// set sensible defaults for delivery/project packages etc if not set
|
|
ConfigModule configModule = new ConfigModule(contextModule, configuration, connectivity);
|
|
immutableConfig = configModule.getConfig();
|
|
logger = immutableConfig.getLogger();
|
|
warnIfNotAppContext(androidContext);
|
|
|
|
// setup storage as soon as possible
|
|
final StorageModule storageModule = new StorageModule(appContext,
|
|
immutableConfig, logger);
|
|
|
|
// setup state trackers for bugsnag
|
|
BugsnagStateModule bugsnagStateModule = new BugsnagStateModule(
|
|
configModule, configuration);
|
|
clientObservable = bugsnagStateModule.getClientObservable();
|
|
callbackState = bugsnagStateModule.getCallbackState();
|
|
breadcrumbState = bugsnagStateModule.getBreadcrumbState();
|
|
contextState = bugsnagStateModule.getContextState();
|
|
metadataState = bugsnagStateModule.getMetadataState();
|
|
featureFlagState = bugsnagStateModule.getFeatureFlagState();
|
|
|
|
// lookup system services
|
|
final SystemServiceModule systemServiceModule = new SystemServiceModule(contextModule);
|
|
|
|
// block until storage module has resolved everything
|
|
storageModule.resolveDependencies(bgTaskService, TaskType.IO);
|
|
|
|
// setup further state trackers and data collection
|
|
TrackerModule trackerModule = new TrackerModule(configModule,
|
|
storageModule, this, bgTaskService, callbackState);
|
|
launchCrashTracker = trackerModule.getLaunchCrashTracker();
|
|
sessionTracker = trackerModule.getSessionTracker();
|
|
|
|
DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule,
|
|
configModule, systemServiceModule, trackerModule,
|
|
bgTaskService, connectivity, storageModule.getDeviceId(),
|
|
storageModule.getInternalDeviceId(), memoryTrimState);
|
|
dataCollectionModule.resolveDependencies(bgTaskService, TaskType.IO);
|
|
appDataCollector = dataCollectionModule.getAppDataCollector();
|
|
deviceDataCollector = dataCollectionModule.getDeviceDataCollector();
|
|
|
|
// load the device + user information
|
|
userState = storageModule.getUserStore().load(configuration.getUser());
|
|
storageModule.getSharedPrefMigrator().deleteLegacyPrefs();
|
|
|
|
registerLifecycleCallbacks();
|
|
|
|
EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule,
|
|
dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier,
|
|
callbackState);
|
|
eventStorageModule.resolveDependencies(bgTaskService, TaskType.IO);
|
|
eventStore = eventStorageModule.getEventStore();
|
|
|
|
deliveryDelegate = new DeliveryDelegate(logger, eventStore,
|
|
immutableConfig, callbackState, notifier, bgTaskService);
|
|
|
|
// Install a default exception handler with this client
|
|
exceptionHandler = new ExceptionHandler(this, logger);
|
|
if (immutableConfig.getEnabledErrorTypes().getUnhandledExceptions()) {
|
|
exceptionHandler.install();
|
|
}
|
|
|
|
// load last run info
|
|
lastRunInfoStore = storageModule.getLastRunInfoStore();
|
|
lastRunInfo = storageModule.getLastRunInfo();
|
|
|
|
// initialise plugins before attempting to flush any errors
|
|
loadPlugins(configuration);
|
|
|
|
// Flush any on-disk errors and sessions
|
|
eventStore.flushOnLaunch();
|
|
eventStore.flushAsync();
|
|
sessionTracker.flushAsync();
|
|
|
|
// register listeners for system events in the background.
|
|
systemBroadcastReceiver = new SystemBroadcastReceiver(this, logger);
|
|
registerComponentCallbacks();
|
|
registerListenersInBackground();
|
|
|
|
// leave auto breadcrumb
|
|
Map<String, Object> data = Collections.emptyMap();
|
|
leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data);
|
|
logger.d("Bugsnag loaded");
|
|
}
|
|
|
|
@VisibleForTesting
|
|
Client(
|
|
ImmutableConfig immutableConfig,
|
|
MetadataState metadataState,
|
|
ContextState contextState,
|
|
CallbackState callbackState,
|
|
UserState userState,
|
|
FeatureFlagState featureFlagState,
|
|
ClientObservable clientObservable,
|
|
Context appContext,
|
|
@NonNull DeviceDataCollector deviceDataCollector,
|
|
@NonNull AppDataCollector appDataCollector,
|
|
@NonNull BreadcrumbState breadcrumbState,
|
|
@NonNull EventStore eventStore,
|
|
SystemBroadcastReceiver systemBroadcastReceiver,
|
|
SessionTracker sessionTracker,
|
|
Connectivity connectivity,
|
|
Logger logger,
|
|
DeliveryDelegate deliveryDelegate,
|
|
LastRunInfoStore lastRunInfoStore,
|
|
LaunchCrashTracker launchCrashTracker,
|
|
ExceptionHandler exceptionHandler,
|
|
Notifier notifier
|
|
) {
|
|
this.immutableConfig = immutableConfig;
|
|
this.metadataState = metadataState;
|
|
this.contextState = contextState;
|
|
this.callbackState = callbackState;
|
|
this.userState = userState;
|
|
this.featureFlagState = featureFlagState;
|
|
this.clientObservable = clientObservable;
|
|
this.appContext = appContext;
|
|
this.deviceDataCollector = deviceDataCollector;
|
|
this.appDataCollector = appDataCollector;
|
|
this.breadcrumbState = breadcrumbState;
|
|
this.eventStore = eventStore;
|
|
this.systemBroadcastReceiver = systemBroadcastReceiver;
|
|
this.sessionTracker = sessionTracker;
|
|
this.connectivity = connectivity;
|
|
this.logger = logger;
|
|
this.deliveryDelegate = deliveryDelegate;
|
|
this.lastRunInfoStore = lastRunInfoStore;
|
|
this.launchCrashTracker = launchCrashTracker;
|
|
this.lastRunInfo = null;
|
|
this.exceptionHandler = exceptionHandler;
|
|
this.notifier = notifier;
|
|
}
|
|
|
|
void registerLifecycleCallbacks() {
|
|
if (appContext instanceof Application) {
|
|
Application application = (Application) appContext;
|
|
SessionLifecycleCallback sessionCb = new SessionLifecycleCallback(sessionTracker);
|
|
application.registerActivityLifecycleCallbacks(sessionCb);
|
|
|
|
if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) {
|
|
ActivityBreadcrumbCollector activityCb = new ActivityBreadcrumbCollector(
|
|
new Function2<String, Map<String, ? extends Object>, Unit>() {
|
|
@SuppressWarnings("unchecked")
|
|
@Override
|
|
public Unit invoke(String activity, Map<String, ?> metadata) {
|
|
leaveBreadcrumb(activity, (Map<String, Object>) metadata,
|
|
BreadcrumbType.STATE);
|
|
return null;
|
|
}
|
|
}
|
|
);
|
|
application.registerActivityLifecycleCallbacks(activityCb);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers listeners for system events in the background. This offloads work from the main
|
|
* thread that collects useful information from callbacks, but that don't need to be done
|
|
* immediately on client construction.
|
|
*/
|
|
void registerListenersInBackground() {
|
|
try {
|
|
bgTaskService.submitTask(TaskType.DEFAULT, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
connectivity.registerForNetworkChanges();
|
|
SystemBroadcastReceiver.register(appContext, systemBroadcastReceiver, logger);
|
|
}
|
|
});
|
|
} catch (RejectedExecutionException ex) {
|
|
logger.w("Failed to register for system events", ex);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Load information about the last run, and reset the persisted information to the defaults.
|
|
*/
|
|
private void persistRunInfo(final LastRunInfo runInfo) {
|
|
try {
|
|
bgTaskService.submitTask(TaskType.IO, new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
lastRunInfoStore.persist(runInfo);
|
|
}
|
|
});
|
|
} catch (RejectedExecutionException exc) {
|
|
logger.w("Failed to persist last run info", exc);
|
|
}
|
|
}
|
|
|
|
private void loadPlugins(@NonNull final Configuration configuration) {
|
|
NativeInterface.setClient(Client.this);
|
|
Set<Plugin> userPlugins = configuration.getPlugins();
|
|
pluginClient = new PluginClient(userPlugins, immutableConfig, logger);
|
|
pluginClient.loadPlugins(Client.this);
|
|
}
|
|
|
|
private void logNull(String property) {
|
|
logger.e("Invalid null value supplied to client." + property + ", ignoring");
|
|
}
|
|
|
|
private void registerComponentCallbacks() {
|
|
appContext.registerComponentCallbacks(new ClientComponentCallbacks(
|
|
deviceDataCollector,
|
|
new Function2<String, String, Unit>() {
|
|
@Override
|
|
public Unit invoke(String oldOrientation, String newOrientation) {
|
|
Map<String, Object> data = new HashMap<>();
|
|
data.put("from", oldOrientation);
|
|
data.put("to", newOrientation);
|
|
leaveAutoBreadcrumb("Orientation changed", BreadcrumbType.STATE, data);
|
|
clientObservable.postOrientationChange(newOrientation);
|
|
return null;
|
|
}
|
|
}, new Function2<Boolean, Integer, Unit>() {
|
|
@Override
|
|
public Unit invoke(Boolean isLowMemory, Integer memoryTrimLevel) {
|
|
memoryTrimState.setLowMemory(Boolean.TRUE.equals(isLowMemory));
|
|
if (memoryTrimState.updateMemoryTrimLevel(memoryTrimLevel)) {
|
|
leaveAutoBreadcrumb(
|
|
"Trim Memory",
|
|
BreadcrumbType.STATE,
|
|
Collections.<String, Object>singletonMap(
|
|
"trimLevel", memoryTrimState.getTrimLevelDescription()
|
|
)
|
|
);
|
|
}
|
|
|
|
memoryTrimState.emitObservableEvent();
|
|
return null;
|
|
}
|
|
}
|
|
));
|
|
}
|
|
|
|
void setupNdkPlugin() {
|
|
if (!setupNdkDirectory()) {
|
|
logger.w("Failed to setup NDK directory.");
|
|
return;
|
|
}
|
|
|
|
String lastRunInfoPath = lastRunInfoStore.getFile().getAbsolutePath();
|
|
int crashes = (lastRunInfo != null) ? lastRunInfo.getConsecutiveLaunchCrashes() : 0;
|
|
clientObservable.postNdkInstall(immutableConfig, lastRunInfoPath, crashes);
|
|
syncInitialState();
|
|
clientObservable.postNdkDeliverPending();
|
|
}
|
|
|
|
private boolean setupNdkDirectory() {
|
|
try {
|
|
return bgTaskService.submitTask(TaskType.IO, new Callable<Boolean>() {
|
|
@Override
|
|
public Boolean call() {
|
|
File outFile = new File(NativeInterface.getNativeReportPath());
|
|
return outFile.exists() || outFile.mkdirs();
|
|
}
|
|
}).get();
|
|
} catch (Throwable exc) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void addObserver(StateObserver observer) {
|
|
metadataState.addObserver(observer);
|
|
breadcrumbState.addObserver(observer);
|
|
sessionTracker.addObserver(observer);
|
|
clientObservable.addObserver(observer);
|
|
userState.addObserver(observer);
|
|
contextState.addObserver(observer);
|
|
deliveryDelegate.addObserver(observer);
|
|
launchCrashTracker.addObserver(observer);
|
|
memoryTrimState.addObserver(observer);
|
|
featureFlagState.addObserver(observer);
|
|
}
|
|
|
|
void removeObserver(StateObserver observer) {
|
|
metadataState.removeObserver(observer);
|
|
breadcrumbState.removeObserver(observer);
|
|
sessionTracker.removeObserver(observer);
|
|
clientObservable.removeObserver(observer);
|
|
userState.removeObserver(observer);
|
|
contextState.removeObserver(observer);
|
|
deliveryDelegate.removeObserver(observer);
|
|
launchCrashTracker.removeObserver(observer);
|
|
memoryTrimState.removeObserver(observer);
|
|
featureFlagState.removeObserver(observer);
|
|
}
|
|
|
|
/**
|
|
* Sends initial state values for Metadata/User/Context to any registered observers.
|
|
*/
|
|
void syncInitialState() {
|
|
metadataState.emitObservableEvent();
|
|
contextState.emitObservableEvent();
|
|
userState.emitObservableEvent();
|
|
memoryTrimState.emitObservableEvent();
|
|
featureFlagState.emitObservableEvent();
|
|
}
|
|
|
|
/**
|
|
* Starts tracking a new session. You should disable automatic session tracking via
|
|
* {@link Configuration#setAutoTrackSessions(boolean)} if you call this method.
|
|
* <p/>
|
|
* You should call this at the appropriate time in your application when you wish to start a
|
|
* session. Any subsequent errors which occur in your application will still be reported to
|
|
* Bugsnag but will not count towards your application's
|
|
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
|
|
* stability score</a>. This will start a new session even if there is already an existing
|
|
* session; you should call {@link #resumeSession()} if you only want to start a session
|
|
* when one doesn't already exist.
|
|
*
|
|
* @see #resumeSession()
|
|
* @see #pauseSession()
|
|
* @see Configuration#setAutoTrackSessions(boolean)
|
|
*/
|
|
public void startSession() {
|
|
sessionTracker.startSession(false);
|
|
}
|
|
|
|
/**
|
|
* Pauses tracking of a session. You should disable automatic session tracking via
|
|
* {@link Configuration#setAutoTrackSessions(boolean)} if you call this method.
|
|
* <p/>
|
|
* You should call this at the appropriate time in your application when you wish to pause a
|
|
* session. Any subsequent errors which occur in your application will still be reported to
|
|
* Bugsnag but will not count towards your application's
|
|
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
|
|
* stability score</a>. This can be advantageous if, for example, you do not wish the
|
|
* stability score to include crashes in a background service.
|
|
*
|
|
* @see #startSession()
|
|
* @see #resumeSession()
|
|
* @see Configuration#setAutoTrackSessions(boolean)
|
|
*/
|
|
public void pauseSession() {
|
|
sessionTracker.pauseSession();
|
|
}
|
|
|
|
/**
|
|
* Resumes a session which has previously been paused, or starts a new session if none exists.
|
|
* If a session has already been resumed or started and has not been paused, calling this
|
|
* method will have no effect. You should disable automatic session tracking via
|
|
* {@link Configuration#setAutoTrackSessions(boolean)} if you call this method.
|
|
* <p/>
|
|
* It's important to note that sessions are stored in memory for the lifetime of the
|
|
* application process and are not persisted on disk. Therefore calling this method on app
|
|
* startup would start a new session, rather than continuing any previous session.
|
|
* <p/>
|
|
* You should call this at the appropriate time in your application when you wish to resume
|
|
* a previously started session. Any subsequent errors which occur in your application will
|
|
* still be reported to Bugsnag but will not count towards your application's
|
|
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
|
|
* stability score</a>.
|
|
*
|
|
* @see #startSession()
|
|
* @see #pauseSession()
|
|
* @see Configuration#setAutoTrackSessions(boolean)
|
|
*
|
|
* @return true if a previous session was resumed, false if a new session was started.
|
|
*/
|
|
public boolean resumeSession() {
|
|
return sessionTracker.resumeSession();
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* 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.
|
|
*/
|
|
@Nullable public String getContext() {
|
|
return contextState.getContext();
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* 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.
|
|
*/
|
|
public void setContext(@Nullable String context) {
|
|
contextState.setManualContext(context);
|
|
}
|
|
|
|
/**
|
|
* Sets the user associated with the event.
|
|
*/
|
|
@Override
|
|
public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) {
|
|
userState.setUser(new User(id, email, name));
|
|
}
|
|
|
|
/**
|
|
* Returns the currently set User information.
|
|
*/
|
|
@NonNull
|
|
@Override
|
|
public User getUser() {
|
|
return userState.getUser();
|
|
}
|
|
|
|
/**
|
|
* Add a "on error" callback, to execute code at the point where an error report is
|
|
* captured in Bugsnag.
|
|
*
|
|
* You can use this to add or modify information attached to an Event
|
|
* before it is sent to your dashboard. You can also return
|
|
* <code>false</code> from any callback to prevent delivery. "on error"
|
|
* callbacks do not run before reports generated in the event
|
|
* of immediate app termination from crashes in C/C++ code.
|
|
*
|
|
* For example:
|
|
*
|
|
* Bugsnag.addOnError(new OnErrorCallback() {
|
|
* public boolean run(Event event) {
|
|
* event.setSeverity(Severity.INFO);
|
|
* return true;
|
|
* }
|
|
* })
|
|
*
|
|
* @param onError a callback to run before sending errors to Bugsnag
|
|
* @see OnErrorCallback
|
|
*/
|
|
@Override
|
|
public void addOnError(@NonNull OnErrorCallback onError) {
|
|
if (onError != null) {
|
|
callbackState.addOnError(onError);
|
|
} else {
|
|
logNull("addOnError");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes a previously added "on error" callback
|
|
* @param onError the callback to remove
|
|
*/
|
|
@Override
|
|
public void removeOnError(@NonNull OnErrorCallback onError) {
|
|
if (onError != null) {
|
|
callbackState.removeOnError(onError);
|
|
} else {
|
|
logNull("removeOnError");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add an "on breadcrumb" callback, to execute code before every
|
|
* breadcrumb captured 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.
|
|
*
|
|
* For example:
|
|
*
|
|
* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
|
|
* public boolean run(Breadcrumb breadcrumb) {
|
|
* return false; // ignore the breadcrumb
|
|
* }
|
|
* })
|
|
*
|
|
* @param onBreadcrumb a callback to run before a breadcrumb is captured
|
|
* @see OnBreadcrumbCallback
|
|
*/
|
|
@Override
|
|
public void addOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
|
|
if (onBreadcrumb != null) {
|
|
callbackState.addOnBreadcrumb(onBreadcrumb);
|
|
} else {
|
|
logNull("addOnBreadcrumb");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes a previously added "on breadcrumb" callback
|
|
* @param onBreadcrumb the callback to remove
|
|
*/
|
|
@Override
|
|
public void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
|
|
if (onBreadcrumb != null) {
|
|
callbackState.removeOnBreadcrumb(onBreadcrumb);
|
|
} else {
|
|
logNull("removeOnBreadcrumb");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add an "on session" callback, to execute code before every
|
|
* session captured 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.
|
|
*
|
|
* For example:
|
|
*
|
|
* Bugsnag.onSession(new OnSessionCallback() {
|
|
* public boolean run(Session session) {
|
|
* return false; // ignore the session
|
|
* }
|
|
* })
|
|
*
|
|
* @param onSession a callback to run before a session is captured
|
|
* @see OnSessionCallback
|
|
*/
|
|
@Override
|
|
public void addOnSession(@NonNull OnSessionCallback onSession) {
|
|
if (onSession != null) {
|
|
callbackState.addOnSession(onSession);
|
|
} else {
|
|
logNull("addOnSession");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes a previously added "on session" callback
|
|
* @param onSession the callback to remove
|
|
*/
|
|
@Override
|
|
public void removeOnSession(@NonNull OnSessionCallback onSession) {
|
|
if (onSession != null) {
|
|
callbackState.removeOnSession(onSession);
|
|
} else {
|
|
logNull("removeOnSession");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify Bugsnag of a handled exception
|
|
*
|
|
* @param exception the exception to send to Bugsnag
|
|
*/
|
|
public void notify(@NonNull Throwable exception) {
|
|
notify(exception, null);
|
|
}
|
|
|
|
/**
|
|
* Notify Bugsnag of a handled exception
|
|
*
|
|
* @param exc the exception to send to Bugsnag
|
|
* @param onError callback invoked on the generated error report for
|
|
* additional modification
|
|
*/
|
|
public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) {
|
|
if (exc != null) {
|
|
if (immutableConfig.shouldDiscardError(exc)) {
|
|
return;
|
|
}
|
|
SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION);
|
|
Metadata metadata = metadataState.getMetadata();
|
|
FeatureFlags featureFlags = featureFlagState.getFeatureFlags();
|
|
Event event = new Event(exc, immutableConfig, severityReason, metadata, featureFlags,
|
|
logger);
|
|
populateAndNotifyAndroidEvent(event, onError);
|
|
} else {
|
|
logNull("notify");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Caches an error then attempts to notify.
|
|
*
|
|
* Should only ever be called from the {@link ExceptionHandler}.
|
|
*/
|
|
void notifyUnhandledException(@NonNull Throwable exc, Metadata metadata,
|
|
@SeverityReason.SeverityReasonType String severityReason,
|
|
@Nullable String attributeValue) {
|
|
SeverityReason handledState
|
|
= SeverityReason.newInstance(severityReason, Severity.ERROR, attributeValue);
|
|
Metadata data = Metadata.Companion.merge(metadataState.getMetadata(), metadata);
|
|
Event event = new Event(exc, immutableConfig, handledState,
|
|
data, featureFlagState.getFeatureFlags(), logger);
|
|
populateAndNotifyAndroidEvent(event, null);
|
|
|
|
// persist LastRunInfo so that on relaunch users can check the app crashed
|
|
int consecutiveLaunchCrashes = lastRunInfo == null ? 0
|
|
: lastRunInfo.getConsecutiveLaunchCrashes();
|
|
boolean launching = launchCrashTracker.isLaunching();
|
|
if (launching) {
|
|
consecutiveLaunchCrashes += 1;
|
|
}
|
|
LastRunInfo runInfo = new LastRunInfo(consecutiveLaunchCrashes, true, launching);
|
|
persistRunInfo(runInfo);
|
|
|
|
// suspend execution of any further background tasks, waiting for previously
|
|
// submitted ones to complete.
|
|
bgTaskService.shutdown();
|
|
}
|
|
|
|
void populateAndNotifyAndroidEvent(@NonNull Event event,
|
|
@Nullable OnErrorCallback onError) {
|
|
// Capture the state of the app and device and attach diagnostics to the event
|
|
event.setDevice(deviceDataCollector.generateDeviceWithState(new Date().getTime()));
|
|
event.addMetadata("device", deviceDataCollector.getDeviceMetadata());
|
|
|
|
// add additional info that belongs in metadata
|
|
// generate new object each time, as this can be mutated by end-users
|
|
event.setApp(appDataCollector.generateAppWithState());
|
|
event.addMetadata("app", appDataCollector.getAppDataMetadata());
|
|
|
|
// Attach breadcrumbState to the event
|
|
event.setBreadcrumbs(breadcrumbState.copy());
|
|
|
|
// Attach user info to the event
|
|
User user = userState.getUser();
|
|
event.setUser(user.getId(), user.getEmail(), user.getName());
|
|
|
|
// Attach context to the event
|
|
event.setContext(contextState.getContext());
|
|
notifyInternal(event, onError);
|
|
}
|
|
|
|
void notifyInternal(@NonNull Event event,
|
|
@Nullable OnErrorCallback onError) {
|
|
// set the redacted keys on the event as this
|
|
// will not have been set for RN/Unity events
|
|
Collection<String> redactedKeys = metadataState.getMetadata().getRedactedKeys();
|
|
event.setRedactedKeys(redactedKeys);
|
|
|
|
// get session for event
|
|
Session currentSession = sessionTracker.getCurrentSession();
|
|
|
|
if (currentSession != null
|
|
&& (immutableConfig.getAutoTrackSessions() || !currentSession.isAutoCaptured())) {
|
|
event.setSession(currentSession);
|
|
}
|
|
|
|
// Run on error tasks, don't notify if any return false
|
|
if (!callbackState.runOnErrorTasks(event, logger)
|
|
|| (onError != null
|
|
&& !onError.onError(event))) {
|
|
logger.d("Skipping notification - onError task returned false");
|
|
return;
|
|
}
|
|
|
|
// leave an error breadcrumb of this event - for the next event
|
|
leaveErrorBreadcrumb(event);
|
|
|
|
deliveryDelegate.deliver(event);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* set in {@link Configuration#getMaxBreadcrumbs()}.
|
|
*
|
|
* 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
|
|
* use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and
|
|
* {@link Configuration#addOnBreadcrumb(OnBreadcrumbCallback)} instead.
|
|
*
|
|
* @return a list of collected breadcrumbs
|
|
*/
|
|
@NonNull
|
|
public List<Breadcrumb> getBreadcrumbs() {
|
|
return breadcrumbState.copy();
|
|
}
|
|
|
|
@NonNull
|
|
AppDataCollector getAppDataCollector() {
|
|
return appDataCollector;
|
|
}
|
|
|
|
@NonNull
|
|
DeviceDataCollector getDeviceDataCollector() {
|
|
return deviceDataCollector;
|
|
}
|
|
|
|
/**
|
|
* Adds a map of multiple metadata key-value pairs to the specified section.
|
|
*/
|
|
@Override
|
|
public void addMetadata(@NonNull String section, @NonNull Map<String, ?> value) {
|
|
if (section != null && value != null) {
|
|
metadataState.addMetadata(section, value);
|
|
} else {
|
|
logNull("addMetadata");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the specified key and value in the specified section. The value can be of
|
|
* any primitive type or a collection such as a map, set or array.
|
|
*/
|
|
@Override
|
|
public void addMetadata(@NonNull String section, @NonNull String key, @Nullable Object value) {
|
|
if (section != null && key != null) {
|
|
metadataState.addMetadata(section, key, value);
|
|
|
|
} else {
|
|
logNull("addMetadata");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes all the data from the specified section.
|
|
*/
|
|
@Override
|
|
public void clearMetadata(@NonNull String section) {
|
|
if (section != null) {
|
|
metadataState.clearMetadata(section);
|
|
} else {
|
|
logNull("clearMetadata");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes data with the specified key from the specified section.
|
|
*/
|
|
@Override
|
|
public void clearMetadata(@NonNull String section, @NonNull String key) {
|
|
if (section != null && key != null) {
|
|
metadataState.clearMetadata(section, key);
|
|
} else {
|
|
logNull("clearMetadata");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a map of data in the specified section.
|
|
*/
|
|
@Nullable
|
|
@Override
|
|
public Map<String, Object> getMetadata(@NonNull String section) {
|
|
if (section != null) {
|
|
return metadataState.getMetadata(section);
|
|
} else {
|
|
logNull("getMetadata");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the value of the specified key in the specified section.
|
|
*/
|
|
@Override
|
|
@Nullable
|
|
public Object getMetadata(@NonNull String section, @NonNull String key) {
|
|
if (section != null && key != null) {
|
|
return metadataState.getMetadata(section, key);
|
|
} else {
|
|
logNull("getMetadata");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// cast map to retain original signature until next major version bump, as this
|
|
// method signature is used by Unity/React native
|
|
@NonNull
|
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
Map<String, Object> getMetadata() {
|
|
return (Map) metadataState.getMetadata().toMap();
|
|
}
|
|
|
|
/**
|
|
* Leave a "breadcrumb" log message, representing an action that occurred
|
|
* in your app, to aid with debugging.
|
|
*
|
|
* @param message the log message to leave
|
|
*/
|
|
public void leaveBreadcrumb(@NonNull String message) {
|
|
if (message != null) {
|
|
breadcrumbState.add(new Breadcrumb(message, logger));
|
|
} else {
|
|
logNull("leaveBreadcrumb");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Leave a "breadcrumb" log message representing an action or event which
|
|
* occurred in your app, to aid with debugging
|
|
* @param message A short label
|
|
* @param metadata Additional diagnostic information about the app environment
|
|
* @param type A category for the breadcrumb
|
|
*/
|
|
public void leaveBreadcrumb(@NonNull String message,
|
|
@NonNull Map<String, Object> metadata,
|
|
@NonNull BreadcrumbType type) {
|
|
if (message != null && type != null && metadata != null) {
|
|
breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger));
|
|
} else {
|
|
logNull("leaveBreadcrumb");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Intended for internal use only - leaves a breadcrumb if the type is enabled for automatic
|
|
* breadcrumbs.
|
|
*
|
|
* @param message A short label
|
|
* @param type A category for the breadcrumb
|
|
* @param metadata Additional diagnostic information about the app environment
|
|
*/
|
|
void leaveAutoBreadcrumb(@NonNull String message,
|
|
@NonNull BreadcrumbType type,
|
|
@NonNull Map<String, Object> metadata) {
|
|
if (!immutableConfig.shouldDiscardBreadcrumb(type)) {
|
|
breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger));
|
|
}
|
|
}
|
|
|
|
private void leaveErrorBreadcrumb(@NonNull Event event) {
|
|
// Add a breadcrumb for this event occurring
|
|
List<Error> errors = event.getErrors();
|
|
|
|
if (errors.size() > 0) {
|
|
String errorClass = errors.get(0).getErrorClass();
|
|
String message = errors.get(0).getErrorMessage();
|
|
|
|
Map<String, Object> data = new HashMap<>();
|
|
data.put("errorClass", errorClass);
|
|
data.put("message", message);
|
|
data.put("unhandled", String.valueOf(event.isUnhandled()));
|
|
data.put("severity", event.getSeverity().toString());
|
|
breadcrumbState.add(new Breadcrumb(errorClass,
|
|
BreadcrumbType.ERROR, data, new Date(), logger));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public void addFeatureFlag(@NonNull String name) {
|
|
if (name != null) {
|
|
featureFlagState.addFeatureFlag(name);
|
|
} else {
|
|
logNull("addFeatureFlag");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public void addFeatureFlag(@NonNull String name, @Nullable String variant) {
|
|
if (name != null) {
|
|
featureFlagState.addFeatureFlag(name, variant);
|
|
} else {
|
|
logNull("addFeatureFlag");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public void addFeatureFlags(@NonNull Iterable<FeatureFlag> featureFlags) {
|
|
if (featureFlags != null) {
|
|
featureFlagState.addFeatureFlags(featureFlags);
|
|
} else {
|
|
logNull("addFeatureFlags");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public void clearFeatureFlag(@NonNull String name) {
|
|
if (name != null) {
|
|
featureFlagState.clearFeatureFlag(name);
|
|
} else {
|
|
logNull("clearFeatureFlag");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public void clearFeatureFlags() {
|
|
featureFlagState.clearFeatureFlags();
|
|
}
|
|
|
|
/**
|
|
* Retrieves information about the last launch of the application, if it has been run before.
|
|
*
|
|
* 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
|
|
* app data cache.
|
|
*/
|
|
@Nullable
|
|
public LastRunInfo getLastRunInfo() {
|
|
return lastRunInfo;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
* and synchronous delivery will not be attempted on the next launch for any fatal crashes.
|
|
*
|
|
* By default this method will be called after Bugsnag is initialized when
|
|
* {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually
|
|
* has precedence over the value supplied via the launchDurationMillis configuration option.
|
|
*/
|
|
public void markLaunchCompleted() {
|
|
launchCrashTracker.markLaunchCompleted();
|
|
}
|
|
|
|
SessionTracker getSessionTracker() {
|
|
return sessionTracker;
|
|
}
|
|
|
|
@NonNull
|
|
EventStore getEventStore() {
|
|
return eventStore;
|
|
}
|
|
|
|
/**
|
|
* Finalize by removing the receiver
|
|
*
|
|
* @throws Throwable if something goes wrong
|
|
*/
|
|
@SuppressWarnings("checkstyle:NoFinalizer")
|
|
protected void finalize() throws Throwable {
|
|
if (systemBroadcastReceiver != null) {
|
|
try {
|
|
ContextExtensionsKt.unregisterReceiverSafe(appContext,
|
|
systemBroadcastReceiver, logger);
|
|
} catch (IllegalArgumentException exception) {
|
|
logger.w("Receiver not registered");
|
|
}
|
|
}
|
|
super.finalize();
|
|
}
|
|
|
|
private void warnIfNotAppContext(Context androidContext) {
|
|
if (!(androidContext instanceof Application)) {
|
|
logger.w("You should initialize Bugsnag from the onCreate() callback of your "
|
|
+ "Application subclass, as this guarantees errors are captured as early "
|
|
+ "as possible. "
|
|
+ "If a custom Application subclass is not possible in your app then you "
|
|
+ "should suppress this warning by passing the Application context instead: "
|
|
+ "Bugsnag.start(context.getApplicationContext()). "
|
|
+ "For further info see: "
|
|
+ "https://docs.bugsnag.com/platforms/android/#basic-configuration");
|
|
|
|
}
|
|
}
|
|
|
|
ImmutableConfig getConfig() {
|
|
return immutableConfig;
|
|
}
|
|
|
|
void setBinaryArch(String binaryArch) {
|
|
getAppDataCollector().setBinaryArch(binaryArch);
|
|
}
|
|
|
|
Context getAppContext() {
|
|
return appContext;
|
|
}
|
|
|
|
/**
|
|
* Intended for internal use only - sets the code bundle id for React Native
|
|
*/
|
|
@Nullable
|
|
String getCodeBundleId() {
|
|
return appDataCollector.getCodeBundleId();
|
|
}
|
|
|
|
/**
|
|
* Intended for internal use only - sets the code bundle id for React Native
|
|
*/
|
|
void setCodeBundleId(@Nullable String codeBundleId) {
|
|
appDataCollector.setCodeBundleId(codeBundleId);
|
|
}
|
|
|
|
void addRuntimeVersionInfo(@NonNull String key, @NonNull String value) {
|
|
deviceDataCollector.addRuntimeVersionInfo(key, value);
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void close() {
|
|
connectivity.unregisterForNetworkChanges();
|
|
bgTaskService.shutdown();
|
|
}
|
|
|
|
Logger getLogger() {
|
|
return logger;
|
|
}
|
|
|
|
/**
|
|
* Retrieves an instantiated plugin of the given type, or null if none has been created
|
|
*/
|
|
@SuppressWarnings("rawtypes")
|
|
@Nullable
|
|
Plugin getPlugin(@NonNull Class clz) {
|
|
return pluginClient.findPlugin(clz);
|
|
}
|
|
|
|
Notifier getNotifier() {
|
|
return notifier;
|
|
}
|
|
|
|
MetadataState getMetadataState() {
|
|
return metadataState;
|
|
}
|
|
|
|
FeatureFlagState getFeatureFlagState() {
|
|
return featureFlagState;
|
|
}
|
|
|
|
ContextState getContextState() {
|
|
return contextState;
|
|
}
|
|
|
|
void setAutoNotify(boolean autoNotify) {
|
|
pluginClient.setAutoNotify(this, autoNotify);
|
|
|
|
if (autoNotify) {
|
|
exceptionHandler.install();
|
|
} else {
|
|
exceptionHandler.uninstall();
|
|
}
|
|
}
|
|
|
|
void setAutoDetectAnrs(boolean autoDetectAnrs) {
|
|
pluginClient.setAutoDetectAnrs(this, autoDetectAnrs);
|
|
}
|
|
}
|