package com.bugsnag.android; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import java.util.Locale; import java.util.Collections; import java.util.Map; import java.util.Observable; import java.util.Observer; enum DeliveryStyle { SAME_THREAD, ASYNC, ASYNC_WITH_CACHE } /** * 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 */ public class Client extends Observable implements Observer { private static final boolean BLOCKING = true; private static final String SHARED_PREF_KEY = "com.bugsnag.android"; private static final String USER_ID_KEY = "user.id"; private static final String USER_NAME_KEY = "user.name"; private static final String USER_EMAIL_KEY = "user.email"; protected final Configuration config; private final Context appContext; protected final AppData appData; protected final DeviceData deviceData; final Breadcrumbs breadcrumbs; protected final User user = new User(); protected final ErrorStore errorStore; /** * Initialize a Bugsnag client * * @param androidContext an Android context, usually <code>this</code> */ public Client(@NonNull Context androidContext) { this(androidContext, null, true); } /** * 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, @Nullable String apiKey) { this(androidContext, apiKey, true); } /** * Initialize a Bugsnag client * * @param androidContext an Android context, usually <code>this</code> * @param apiKey your Bugsnag API key from your Bugsnag dashboard * @param enableExceptionHandler should we automatically handle uncaught exceptions? */ public Client(@NonNull Context androidContext, @Nullable String apiKey, boolean enableExceptionHandler) { this(androidContext, createNewConfiguration(androidContext, apiKey, enableExceptionHandler)); } /** * 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 Configuration configuration) { appContext = androidContext.getApplicationContext(); config = configuration; String buildUUID = null; try { ApplicationInfo ai = appContext.getPackageManager().getApplicationInfo(appContext.getPackageName(), PackageManager.GET_META_DATA); buildUUID = ai.metaData.getString("com.bugsnag.android.BUILD_UUID"); } catch (Exception ignore) { } if (buildUUID != null) { config.setBuildUUID(buildUUID); } // Set up and collect constant app and device diagnostics appData = new AppData(appContext, config); deviceData = new DeviceData(appContext); AppState.init(); // Set up breadcrumbs breadcrumbs = new Breadcrumbs(); // Set sensible defaults setProjectPackages(appContext.getPackageName()); if (config.getPersistUserBetweenSessions()) { // Check to see if a user was stored in the SharedPreferences SharedPreferences sharedPref = appContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); user.setId(sharedPref.getString(USER_ID_KEY, deviceData.getUserId())); user.setName(sharedPref.getString(USER_NAME_KEY, null)); user.setEmail(sharedPref.getString(USER_EMAIL_KEY, null)); } else { user.setId(deviceData.getUserId()); } // Create the error store that is used in the exception handler errorStore = new ErrorStore(config, appContext); // Install a default exception handler with this client if (config.getEnableExceptionHandler()) { enableExceptionHandler(); } config.addObserver(this); // Flush any on-disk errors errorStore.flush(); } public void notifyBugsnagObservers(NotifyType type) { setChanged(); super.notifyObservers(type.getValue()); } @Override public void update(Observable o, Object arg) { if (arg instanceof Integer) { NotifyType type = NotifyType.fromInt((Integer) arg); if (type != null) { notifyBugsnagObservers(type); } } } /** * Creates a new configuration object based on the provided parameters * will read the API key from the manifest file if it is not provided * * @param androidContext The context of the application * @param apiKey The API key to use * @param enableExceptionHandler should we automatically handle uncaught exceptions? * @return The created config */ private static Configuration createNewConfiguration(@NonNull Context androidContext, String apiKey, boolean enableExceptionHandler) { Context appContext = androidContext.getApplicationContext(); // Attempt to load API key from AndroidManifest.xml if not passed in if (TextUtils.isEmpty(apiKey)) { try { ApplicationInfo ai = appContext.getPackageManager().getApplicationInfo(appContext.getPackageName(), PackageManager.GET_META_DATA); apiKey = ai.metaData.getString("com.bugsnag.android.API_KEY"); } catch (Exception ignore) { } } if (apiKey == null) { throw new NullPointerException("You must provide a Bugsnag API key"); } // Build a configuration object Configuration newConfig = new Configuration(apiKey); newConfig.setEnableExceptionHandler(enableExceptionHandler); return newConfig; } /** * Set the application version sent to Bugsnag. By default we'll pull this * from your AndroidManifest.xml * * @param appVersion the app version to send */ public void setAppVersion(String appVersion) { config.setAppVersion(appVersion); } /** * Gets the context to be sent to Bugsnag. * * @return Context */ public String getContext() { return config.getContext(); } /** * Set the context sent to Bugsnag. By default we'll attempt to detect the * name of the top-most activity at the time of a report, and use this * as the context, but sometime this is not possible. * * @param context set what was happening at the time of a crash */ public void setContext(String context) { config.setContext(context); } /** * Set the endpoint to send data to. By default we'll send reports to * the standard https://notify.bugsnag.com endpoint, but you can override * this if you are using Bugsnag Enterprise to point to your own Bugsnag * endpoint. * * @param endpoint the custom endpoint to send report to */ public void setEndpoint(String endpoint) { config.setEndpoint(endpoint); } /** * Set the buildUUID to your own value. This is used to identify proguard * mapping files in the case that you publish multiple different apps with * the same appId and versionCode. The default value is read from the * com.bugsnag.android.BUILD_UUID meta-data field in your app manifest. * * @param buildUUID the buildUUID. */ public void setBuildUUID(final String buildUUID) { config.setBuildUUID(buildUUID); } /** * Set which keys should be filtered when sending metaData to Bugsnag. * Use this when you want to ensure sensitive information, such as passwords * or credit card information is stripped from metaData you send to Bugsnag. * Any keys in metaData which contain these strings will be marked as * [FILTERED] when send to Bugsnag. * <p/> * For example: * <p/> * client.setFilters("password", "credit_card"); * * @param filters a list of keys to filter from metaData */ public void setFilters(String... filters) { config.setFilters(filters); } /** * Set which exception classes should be ignored (not sent) by Bugsnag. * <p/> * For example: * <p/> * client.setIgnoreClasses("java.lang.RuntimeException"); * * @param ignoreClasses a list of exception classes to ignore */ public void setIgnoreClasses(String... ignoreClasses) { config.setIgnoreClasses(ignoreClasses); } /** * Set for which releaseStages errors should be sent to Bugsnag. * Use this to stop errors from development builds being sent. * <p/> * For example: * <p/> * client.setNotifyReleaseStages("production"); * * @param notifyReleaseStages a list of releaseStages to notify for * @see #setReleaseStage */ public void setNotifyReleaseStages(String... notifyReleaseStages) { config.setNotifyReleaseStages(notifyReleaseStages); } /** * Set which packages should be considered part of your application. * Bugsnag uses this to help with error grouping, and stacktrace display. * <p/> * For example: * <p/> * client.setProjectPackages("com.example.myapp"); * <p/> * By default, we'll mark the current package name as part of you app. * * @param projectPackages a list of package names */ public void setProjectPackages(String... projectPackages) { config.setProjectPackages(projectPackages); } /** * Set the current "release stage" of your application. * By default, we'll set this to "development" for debug builds and * "production" for non-debug builds. * * @param releaseStage the release stage of the app * @see #setNotifyReleaseStages */ public void setReleaseStage(String releaseStage) { config.setReleaseStage(releaseStage); } /** * Set whether to send thread-state with report. * By default, this will be true. * * @param sendThreads should we send thread-state with report? */ public void setSendThreads(boolean sendThreads) { config.setSendThreads(sendThreads); } /** * Set details of the user currently using your application. * You can search for this information in your Bugsnag dashboard. * <p/> * For example: * <p/> * client.setUser("12345", "james@example.com", "James Smith"); * * @param id a unique identifier of the current user (defaults to a unique id) * @param email the email address of the current user * @param name the name of the current user */ public void setUser(String id, String email, String name) { setUserId(id); setUserEmail(email); setUserName(name); } /** * Removes the current user data and sets it back to defaults */ public void clearUser() { user.setId(deviceData.getUserId()); user.setEmail(null); user.setName(null); SharedPreferences sharedPref = appContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); sharedPref.edit() .remove(USER_ID_KEY) .remove(USER_EMAIL_KEY) .remove(USER_NAME_KEY) .commit(); notifyBugsnagObservers(NotifyType.USER); } /** * Set a unique identifier for the user currently using your application. * By default, this will be an automatically generated unique id * You can search for this information in your Bugsnag dashboard. * * @param id a unique identifier of the current user */ public void setUserId(String id) { setUserId(id, true); } /** * Sets the user ID with the option to not notify any NDK components of the change * * @param id a unique identifier of the current user * @param notify whether or not to notify NDK components */ void setUserId(String id, boolean notify) { user.setId(id); if (config.getPersistUserBetweenSessions()) { storeInSharedPrefs(USER_ID_KEY, id); } if (notify) { notifyBugsnagObservers(NotifyType.USER); } } /** * Set the email address of the current user. * You can search for this information in your Bugsnag dashboard. * * @param email the email address of the current user */ public void setUserEmail(String email) { setUserEmail(email, true); } /** * Sets the user email with the option to not notify any NDK components of the change * * @param email the email address of the current user * @param notify whether or not to notify NDK components */ void setUserEmail(String email, boolean notify) { user.setEmail(email); if (config.getPersistUserBetweenSessions()) { storeInSharedPrefs(USER_EMAIL_KEY, email); } if (notify) { notifyBugsnagObservers(NotifyType.USER); } } /** * Set the name of the current user. * You can search for this information in your Bugsnag dashboard. * * @param name the name of the current user */ public void setUserName(String name) { setUserName(name, true); } /** * Sets the user name with the option to not notify any NDK components of the change * * @param name the name of the current user * @param notify whether or not to notify NDK components */ void setUserName(String name, boolean notify) { user.setName(name); if (config.getPersistUserBetweenSessions()) { storeInSharedPrefs(USER_NAME_KEY, name); } if (notify) { notifyBugsnagObservers(NotifyType.USER); } } /** * Add a "before notify" callback, to execute code before every * report to Bugsnag. * <p/> * You can use this to add or modify information attached to an error * before it is sent to your dashboard. You can also return * <code>false</code> from any callback to halt execution. * <p/> * For example: * <p/> * client.beforeNotify(new BeforeNotify() { * public boolean run(Error error) { * error.setSeverity(Severity.INFO); * return true; * } * }) * * @param beforeNotify a callback to run before sending errors to Bugsnag * @see BeforeNotify */ public void beforeNotify(BeforeNotify beforeNotify) { config.beforeNotify(beforeNotify); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag */ public void notify(Throwable exception) { Error error = new Error(config, exception); notify(error, !BLOCKING); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag */ public void notifyBlocking(Throwable exception) { Error error = new Error(config, exception); notify(error, BLOCKING); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag * @param callback callback invoked on the generated error report for * additional modification */ public void notify(Throwable exception, Callback callback) { Error error = new Error(config, exception); notify(error, DeliveryStyle.ASYNC, callback); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag * @param callback callback invoked on the generated error report for * additional modification */ public void notifyBlocking(Throwable exception, Callback callback) { Error error = new Error(config, exception); notify(error, DeliveryStyle.SAME_THREAD, callback); } /** * Notify Bugsnag of an error * * @param name the error name or class * @param message the error message * @param stacktrace the stackframes associated with the error * @param callback callback invoked on the generated error report for * additional modification */ public void notify(String name, String message, StackTraceElement[] stacktrace, Callback callback) { Error error = new Error(config, name, message, stacktrace); notify(error, DeliveryStyle.ASYNC, callback); } /** * Notify Bugsnag of an error * * @param name the error name or class * @param message the error message * @param stacktrace the stackframes associated with the error * @param callback callback invoked on the generated error report for * additional modification */ public void notifyBlocking(String name, String message, StackTraceElement[] stacktrace, Callback callback) { Error error = new Error(config, name, message, stacktrace); notify(error, DeliveryStyle.SAME_THREAD, callback); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag * @param severity the severity of the error, one of Severity.ERROR, * Severity.WARNING or Severity.INFO */ public void notify(Throwable exception, Severity severity) { Error error = new Error(config, exception); error.setSeverity(severity); notify(error, !BLOCKING); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag * @param severity the severity of the error, one of Severity.ERROR, * Severity.WARNING or Severity.INFO */ public void notifyBlocking(Throwable exception, Severity severity) { Error error = new Error(config, exception); error.setSeverity(severity); notify(error, BLOCKING); } /** * Add diagnostic information to every error report. * Diagnostic information is collected in "tabs" on your dashboard. * <p/> * For example: * <p/> * client.addToTab("account", "name", "Acme Co."); * client.addToTab("account", "payingCustomer", true); * * @param tab the dashboard tab to add diagnostic data to * @param key the name of the diagnostic information * @param value the contents of the diagnostic information */ public void addToTab(String tab, String key, Object value) { config.getMetaData().addToTab(tab, key, value); } /** * Remove a tab of app-wide diagnostic information * * @param tabName the dashboard tab to remove diagnostic data from */ public void clearTab(String tabName) { config.getMetaData().clearTab(tabName); } /** * Get the global diagnostic information currently stored in MetaData. * * @see MetaData */ public MetaData getMetaData() { return config.getMetaData(); } /** * Set the global diagnostic information to be send with every error. * * @see MetaData */ public void setMetaData(MetaData metaData) { config.setMetaData(metaData); } /** * Leave a "breadcrumb" log message, representing an action that occurred * in your app, to aid with debugging. * * @param breadcrumb the log message to leave (max 140 chars) */ public void leaveBreadcrumb(String breadcrumb) { breadcrumbs.add(breadcrumb); notifyBugsnagObservers(NotifyType.BREADCRUMB); } public void leaveBreadcrumb(String name, BreadcrumbType type, Map<String, String> metadata) { leaveBreadcrumb(name, type, metadata, true); } void leaveBreadcrumb(String name, BreadcrumbType type, Map<String, String> metadata, boolean notify) { breadcrumbs.add(name, type, metadata); if (notify) { notifyBugsnagObservers(NotifyType.BREADCRUMB); } } /** * Set the maximum number of breadcrumbs to keep and sent to Bugsnag. * By default, we'll keep and send the 20 most recent breadcrumb log * messages. * * @param numBreadcrumbs number of breadcrumb log messages to send */ public void setMaxBreadcrumbs(int numBreadcrumbs) { breadcrumbs.setSize(numBreadcrumbs); } /** * Clear any breadcrumbs that have been left so far. */ public void clearBreadcrumbs() { breadcrumbs.clear(); notifyBugsnagObservers(NotifyType.BREADCRUMB); } /** * Enable automatic reporting of unhandled exceptions. * By default, this is automatically enabled in the constructor. */ public void enableExceptionHandler() { ExceptionHandler.enable(this); } /** * Disable automatic reporting of unhandled exceptions. */ public void disableExceptionHandler() { ExceptionHandler.disable(this); } private void notify(Error error, boolean blocking) { DeliveryStyle style = blocking ? DeliveryStyle.SAME_THREAD : DeliveryStyle.ASYNC; notify(error, style, null); } private void notify(Error error, DeliveryStyle style, Callback callback) { // Don't notify if this error class should be ignored if (error.shouldIgnoreClass()) { return; } // Don't notify unless releaseStage is in notifyReleaseStages if (!config.shouldNotifyForReleaseStage(appData.getReleaseStage())) { return; } // Capture the state of the app and device and attach diagnostics to the error error.setAppData(appData); error.setDeviceData(deviceData); error.setAppState(new AppState(appContext)); error.setDeviceState(new DeviceState(appContext)); // Attach breadcrumbs to the error error.setBreadcrumbs(breadcrumbs); // Attach user info to the error error.setUser(user); // Run beforeNotify tasks, don't notify if any return true if (!runBeforeNotifyTasks(error)) { Logger.info("Skipping notification - beforeNotify task returned false"); return; } // Build the report Report report = new Report(config.getApiKey(), error); if (callback != null) { callback.beforeNotify(report); } switch (style) { case SAME_THREAD: deliver(report, error); break; case ASYNC: final Report finalReport = report; final Error finalError = error; // Attempt to send the report in the background Async.run(new Runnable() { @Override public void run() { deliver(finalReport, finalError); } }); break; case ASYNC_WITH_CACHE: errorStore.write(error); errorStore.flush(); } // Add a breadcrumb for this error occurring breadcrumbs.add(error.getExceptionName(), BreadcrumbType.ERROR, Collections.singletonMap("message", error.getExceptionMessage())); } void deliver(Report report, Error error) { try { HttpClient.post(config.getEndpoint(), report); Logger.info(String.format(Locale.US, "Sent 1 new error to Bugsnag")); } catch (HttpClient.NetworkException e) { Logger.info("Could not send error(s) to Bugsnag, saving to disk to send later"); // Save error to disk for later sending errorStore.write(error); } catch (HttpClient.BadResponseException e) { Logger.info("Bad response when sending data to Bugsnag"); } catch (Exception e) { Logger.warn("Problem sending error to Bugsnag", e); } } void cacheAndNotify(Throwable exception, Severity severity) { Error error = new Error(config, exception); error.setSeverity(severity); notify(error, DeliveryStyle.ASYNC_WITH_CACHE, null); } private boolean runBeforeNotifyTasks(Error error) { for (BeforeNotify beforeNotify : config.getBeforeNotifyTasks()) { try { if (!beforeNotify.run(error)) { return false; } } catch (Throwable ex) { Logger.warn("BeforeNotify threw an Exception", ex); } } // By default, allow the error to be sent if there were no objections return true; } /** * Stores the given key value pair into shared preferences * @param key The key to store * @param value The value to store * @return Whether the value was stored successfully or not */ private boolean storeInSharedPrefs(String key, String value) { SharedPreferences sharedPref = appContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); return sharedPref.edit().putString(key, value).commit(); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag * @param metaData additional information to send with the exception * * @deprecated Use {@link #notify(Throwable,Callback)} * to send and modify error reports */ public void notify(Throwable exception, MetaData metaData) { Error error = new Error(config, exception); error.setMetaData(metaData); notify(error, !BLOCKING); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag * @param metaData additional information to send with the exception * * @deprecated Use {@link #notify(Throwable,Callback)} * to send and modify error reports */ public void notifyBlocking(Throwable exception, MetaData metaData) { Error error = new Error(config, exception); error.setMetaData(metaData); notify(error, BLOCKING); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag * @param severity the severity of the error, one of Severity.ERROR, * Severity.WARNING or Severity.INFO * @param metaData additional information to send with the exception * * @deprecated Use {@link #notify(Throwable,Callback)} to send and * modify error reports */ @Deprecated public void notify(Throwable exception, Severity severity, MetaData metaData) { Error error = new Error(config, exception); error.setSeverity(severity); error.setMetaData(metaData); notify(error, !BLOCKING); } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag * @param severity the severity of the error, one of Severity.ERROR, * Severity.WARNING or Severity.INFO * @param metaData additional information to send with the exception * * @deprecated Use {@link #notifyBlocking(Throwable,Callback)} to send * and modify error reports */ @Deprecated public void notifyBlocking(Throwable exception, Severity severity, MetaData metaData) { Error error = new Error(config, exception); error.setSeverity(severity); error.setMetaData(metaData); notify(error, BLOCKING); } /** * Notify Bugsnag of an error * * @param name the error name or class * @param message the error message * @param stacktrace the stackframes associated with the error * @param severity the severity of the error, one of Severity.ERROR, * Severity.WARNING or Severity.INFO * @param metaData additional information to send with the exception * * @deprecated Use {@link #notify(String,String,StackTraceElement[],Callback)} * to send and modify error reports */ @Deprecated public void notify(String name, String message, StackTraceElement[] stacktrace, Severity severity, MetaData metaData) { Error error = new Error(config, name, message, stacktrace); error.setSeverity(severity); error.setMetaData(metaData); notify(error, !BLOCKING); } /** * Notify Bugsnag of an error * * @param name the error name or class * @param message the error message * @param stacktrace the stackframes associated with the error * @param severity the severity of the error, one of Severity.ERROR, * Severity.WARNING or Severity.INFO * @param metaData additional information to send with the exception * * @deprecated Use {@link #notifyBlocking(String,String,StackTraceElement[],Callback)} * to send and modify error reports */ @Deprecated public void notifyBlocking(String name, String message, StackTraceElement[] stacktrace, Severity severity, MetaData metaData) { Error error = new Error(config, name, message, stacktrace); error.setSeverity(severity); error.setMetaData(metaData); notify(error, BLOCKING); } /** * Notify Bugsnag of an error * * @param name the error name or class * @param message the error message * @param context the error context * @param stacktrace the stackframes associated with the error * @param severity the severity of the error, one of Severity.ERROR, * Severity.WARNING or Severity.INFO * @param metaData additional information to send with the exception * * @deprecated Use {@link #notify(String,String,StackTraceElement[],Callback)} * to send and modify error reports */ @Deprecated public void notify(String name, String message, String context, StackTraceElement[] stacktrace, Severity severity, MetaData metaData) { Error error = new Error(config, name, message, stacktrace); error.setSeverity(severity); error.setMetaData(metaData); error.setContext(context); notify(error, !BLOCKING); } /** * Notify Bugsnag of an error * * @param name the error name or class * @param message the error message * @param context the error context * @param stacktrace the stackframes associated with the error * @param severity the severity of the error, one of Severity.ERROR, * Severity.WARNING or Severity.INFO * @param metaData additional information to send with the exception * * @deprecated Use {@link #notifyBlocking(String,String,StackTraceElement[],Callback)} * to send and modify error reports */ @Deprecated public void notifyBlocking(String name, String message, String context, StackTraceElement[] stacktrace, Severity severity, MetaData metaData) { Error error = new Error(config, name, message, stacktrace); error.setSeverity(severity); error.setMetaData(metaData); error.setContext(context); notify(error, BLOCKING); } }