diff --git a/mazerunner/src/main/java/com/bugsnag/android/TestHarnessHooks.kt b/mazerunner/src/main/java/com/bugsnag/android/TestHarnessHooks.kt index fab29e34ed..6cba832f00 100644 --- a/mazerunner/src/main/java/com/bugsnag/android/TestHarnessHooks.kt +++ b/mazerunner/src/main/java/com/bugsnag/android/TestHarnessHooks.kt @@ -2,8 +2,6 @@ package com.bugsnag.android import android.content.Context import android.net.ConnectivityManager -import com.bugsnag.android.Bugsnag.client -import java.util.* /** * Accesses the session tracker and flushes all stored sessions @@ -12,39 +10,61 @@ internal fun flushAllSessions() { Bugsnag.getClient().sessionTracker.flushStoredSessions() } -internal fun flushErrorStoreAsync(client: Client, apiClient: ErrorReportApiClient) { - client.errorStore.flushAsync(apiClient) +internal fun flushErrorStoreAsync(client: Client) { + client.errorStore.flushAsync() } -internal fun flushErrorStoreOnLaunch(client: Client, apiClient: ErrorReportApiClient) { - client.errorStore.flushOnLaunch(apiClient) +internal fun flushErrorStoreOnLaunch(client: Client) { + client.errorStore.flushOnLaunch() } /** - * Creates an error API client with a 500ms delay, emulating poor network connectivity + * Creates a delivery API client with a 500ms delay, emulating poor network connectivity */ -internal fun createSlowErrorApiClient(context: Context): ErrorReportApiClient { +internal fun createSlowDelivery(context: Context): Delivery { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? - val defaultHttpClient = DefaultHttpClient(cm) - - return ErrorReportApiClient({ url: String?, - report: Report?, - headers: MutableMap? -> - Thread.sleep(500) - defaultHttpClient.postReport(url, report, headers) - }) + val delivery = DefaultDelivery(cm) + + return object : Delivery { + override fun deliver(payload: SessionTrackingPayload?, config: Configuration?) { + Thread.sleep(500) + delivery.deliver(payload, config) + } + + override fun deliver(report: Report?, config: Configuration?) { + Thread.sleep(500) + delivery.deliver(report, config) + } + } } -internal fun createDefaultErrorClient(context: Context): ErrorReportApiClient { +internal fun createDefaultDelivery(context: Context): DefaultDelivery { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? - return DefaultHttpClient(cm) + return DefaultDelivery(cm) } -internal fun createDefaultSessionClient(context: Context): SessionTrackingApiClient { - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? - return DefaultHttpClient(cm) +internal fun createCustomHeaderDelivery(context: Context): Delivery { + return object : Delivery { + val delivery: DefaultDelivery = createDefaultDelivery(context) + + override fun deliver(payload: SessionTrackingPayload?, config: Configuration?) { + deliver(config?.sessionEndpoint, payload, config?.sessionApiHeaders) + } + + override fun deliver(report: Report?, config: Configuration?) { + deliver(config?.endpoint, report, config?.errorApiHeaders) + } + + fun deliver(endpoint: String?, + streamable: JsonStream.Streamable?, + headers: MutableMap?) { + headers!!["Custom-Client"] = "Hello World" + delivery.deliver(endpoint, streamable, headers) + } + } } + internal fun writeErrorToStore(client: Client) { val error = Error.Builder(Configuration("api-key"), RuntimeException(), null).build() client.errorStore.write(error) diff --git a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorConnectivityScenario.kt b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorConnectivityScenario.kt index acaa178216..7ca0ff67b3 100644 --- a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorConnectivityScenario.kt +++ b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorConnectivityScenario.kt @@ -11,13 +11,13 @@ internal class AsyncErrorConnectivityScenario(config: Configuration, context: Context) : Scenario(config, context) { override fun run() { + val delivery = createSlowDelivery(context) + config.delivery = delivery super.run() - val apiClient = createSlowErrorApiClient(context) - Bugsnag.setErrorReportApiClient(apiClient) writeErrorToStore(Bugsnag.getClient()) - flushErrorStoreAsync(Bugsnag.getClient(), apiClient) - flushErrorStoreOnLaunch(Bugsnag.getClient(), apiClient) + flushErrorStoreAsync(Bugsnag.getClient()) + flushErrorStoreOnLaunch(Bugsnag.getClient()) Thread.sleep(50) } diff --git a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorDoubleFlushScenario.kt b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorDoubleFlushScenario.kt index 65b98b0090..4a0a66a230 100644 --- a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorDoubleFlushScenario.kt +++ b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorDoubleFlushScenario.kt @@ -11,13 +11,12 @@ internal class AsyncErrorDoubleFlushScenario(config: Configuration, context: Context) : Scenario(config, context) { override fun run() { + config.delivery = createSlowDelivery(context) super.run() - val apiClient = createSlowErrorApiClient(context) - Bugsnag.setErrorReportApiClient(apiClient) writeErrorToStore(Bugsnag.getClient()) - flushErrorStoreAsync(Bugsnag.getClient(), apiClient) - flushErrorStoreAsync(Bugsnag.getClient(), apiClient) + flushErrorStoreAsync(Bugsnag.getClient()) + flushErrorStoreAsync(Bugsnag.getClient()) Thread.sleep(50) } diff --git a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorLaunchScenario.kt b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorLaunchScenario.kt index 87e5c8cf87..5be180e82d 100644 --- a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorLaunchScenario.kt +++ b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/AsyncErrorLaunchScenario.kt @@ -11,13 +11,12 @@ internal class AsyncErrorLaunchScenario(config: Configuration, context: Context) : Scenario(config, context) { override fun run() { + config.delivery = createSlowDelivery(context) super.run() - val apiClient = createSlowErrorApiClient(context) - Bugsnag.setErrorReportApiClient(apiClient) writeErrorToStore(Bugsnag.getClient()) - flushErrorStoreOnLaunch(Bugsnag.getClient(), apiClient) - flushErrorStoreAsync(Bugsnag.getClient(), apiClient) + flushErrorStoreOnLaunch(Bugsnag.getClient()) + flushErrorStoreAsync(Bugsnag.getClient()) Thread.sleep(50) } diff --git a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientErrorFlushScenario.kt b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientErrorFlushScenario.kt index f3f718a403..f58d75c0b1 100644 --- a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientErrorFlushScenario.kt +++ b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientErrorFlushScenario.kt @@ -3,7 +3,7 @@ package com.bugsnag.android.mazerunner.scenarios import android.content.Context import com.bugsnag.android.Bugsnag import com.bugsnag.android.Configuration -import com.bugsnag.android.createDefaultErrorClient +import com.bugsnag.android.createCustomHeaderDelivery /** * Sends an unhandled exception which is cached on disk to Bugsnag, then sent on a separate launch, @@ -13,14 +13,12 @@ internal class CustomClientErrorFlushScenario(config: Configuration, context: Context) : Scenario(config, context) { override fun run() { + if ("DeliverReports" == eventMetaData) { + config.delivery = createCustomHeaderDelivery(context) + } super.run() - if ("DeliverReports" == eventMetaData) { - Bugsnag.setErrorReportApiClient { urlString, report, headers -> - headers["Custom-Client"] = "Hello World" - createDefaultErrorClient(context).postReport(urlString, report, headers) - } - } else { + if ("DeliverReports" != eventMetaData) { disableAllDelivery() throw RuntimeException("ReportCacheScenario") } diff --git a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientErrorScenario.kt b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientErrorScenario.kt index 1817553824..639319a340 100644 --- a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientErrorScenario.kt +++ b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientErrorScenario.kt @@ -3,7 +3,7 @@ package com.bugsnag.android.mazerunner.scenarios import android.content.Context import com.bugsnag.android.Bugsnag import com.bugsnag.android.Configuration -import com.bugsnag.android.createDefaultErrorClient +import com.bugsnag.android.createCustomHeaderDelivery /** * Sends a handled exception to Bugsnag using a custom API client which modifies the request. @@ -12,12 +12,8 @@ internal class CustomClientErrorScenario(config: Configuration, context: Context) : Scenario(config, context) { override fun run() { + config.delivery = createCustomHeaderDelivery(context) super.run() - - Bugsnag.setErrorReportApiClient { urlString, report, headers -> - headers["Custom-Client"] = "Hello World" - createDefaultErrorClient(context).postReport(urlString, report, headers) - } Bugsnag.notify(RuntimeException("Hello")) } diff --git a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientSessionFlushScenario.kt b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientSessionFlushScenario.kt index bd936019b6..f57b48a781 100644 --- a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientSessionFlushScenario.kt +++ b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientSessionFlushScenario.kt @@ -3,7 +3,8 @@ package com.bugsnag.android.mazerunner.scenarios import android.content.Context import com.bugsnag.android.Bugsnag import com.bugsnag.android.Configuration -import com.bugsnag.android.createDefaultSessionClient +import com.bugsnag.android.createCustomHeaderDelivery +import com.bugsnag.android.createDefaultDelivery /** * Sends a session which is cached on disk to Bugsnag, then sent on a separate launch, @@ -18,12 +19,7 @@ internal class CustomClientSessionFlushScenario(config: Configuration, if ("DeliverSessions" == eventMetaData) { // simulate activity lifecycle callback occurring before api client can be set Bugsnag.startSession() - - Bugsnag.setSessionTrackingApiClient { urlString, report, headers -> - headers["Custom-Client"] = "Hello World" - val sessionClient = createDefaultSessionClient(context) - sessionClient.postSessionTrackingPayload(urlString, report, headers) - } + config.delivery = createCustomHeaderDelivery(context) } else { disableAllDelivery() Bugsnag.startSession() diff --git a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientSessionScenario.kt b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientSessionScenario.kt index 24593e358c..954e3143be 100644 --- a/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientSessionScenario.kt +++ b/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CustomClientSessionScenario.kt @@ -3,7 +3,8 @@ package com.bugsnag.android.mazerunner.scenarios import android.content.Context import com.bugsnag.android.Bugsnag import com.bugsnag.android.Configuration -import com.bugsnag.android.createDefaultSessionClient +import com.bugsnag.android.createCustomHeaderDelivery +import com.bugsnag.android.createDefaultDelivery /** * Sends a session using a custom API client which modifies the request. @@ -12,14 +13,8 @@ internal class CustomClientSessionScenario(config: Configuration, context: Context) : Scenario(config, context) { override fun run() { + config.delivery = createCustomHeaderDelivery(context) super.run() - - Bugsnag.setSessionTrackingApiClient { urlString, report, headers -> - headers["Custom-Client"] = "Hello World" - val sessionClient = createDefaultSessionClient(context) - sessionClient.postSessionTrackingPayload(urlString, report, headers) - } - Bugsnag.startSession() } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/BreadcrumbLifecycleCrashTest.java b/sdk/src/androidTest/java/com/bugsnag/android/BreadcrumbLifecycleCrashTest.java index 3223134e9f..f0f5cd6625 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/BreadcrumbLifecycleCrashTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/BreadcrumbLifecycleCrashTest.java @@ -22,11 +22,10 @@ public class BreadcrumbLifecycleCrashTest { */ @Before public void setUp() throws Exception { - Configuration configuration = new Configuration("api-key"); + Configuration configuration = BugsnagTestUtils.generateConfiguration(); Context context = InstrumentationRegistry.getContext(); SessionStore sessionStore = new SessionStore(configuration, context); - SessionTrackingApiClient apiClient = BugsnagTestUtils.generateSessionTrackingApiClient(); - sessionTracker = new SessionTracker(configuration, null, sessionStore, apiClient); + sessionTracker = new SessionTracker(configuration, null, sessionStore); } @Test diff --git a/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java b/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java index e4f38fe8aa..66b3c38c44 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java @@ -42,8 +42,7 @@ static SharedPreferences getSharedPrefs(Context context) { static Client generateClient() { Client client = new Client(InstrumentationRegistry.getContext(), "api-key"); - client.setErrorReportApiClient(generateErrorReportApiClient()); - client.setSessionTrackingApiClient(generateSessionTrackingApiClient()); + client.config.setDelivery(generateDelivery()); return client; } @@ -52,12 +51,14 @@ static Session generateSession() { } static Configuration generateConfiguration() { - return new Configuration("test"); + Configuration configuration = new Configuration("test"); + configuration.setDelivery(generateDelivery()); + return configuration; } static SessionTracker generateSessionTracker() { return new SessionTracker(generateConfiguration(), BugsnagTestUtils.generateClient(), - generateSessionStore(), generateSessionTrackingApiClient()); + generateSessionStore()); } @NonNull @@ -89,4 +90,16 @@ public void postReport(String urlString, } }; } + + public static Delivery generateDelivery() { + return new Delivery() { + @Override + public void deliver(SessionTrackingPayload payload, + Configuration config) throws DeliveryFailureException {} + + @Override + public void deliver(Report report, + Configuration config) throws DeliveryFailureException {} + }; + } } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ClientConfigTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ClientConfigTest.java index 875a1d571c..ef7e19a03e 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ClientConfigTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ClientConfigTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import android.content.Context; import android.support.test.InstrumentationRegistry; @@ -86,4 +87,26 @@ public void testSetSendThreads() throws Exception { assertFalse(config.getSendThreads()); } + @Test + public void testDefaultClientDelivery() { + assertFalse(client.config.getDelivery() instanceof DeliveryCompat); + } + + @Test + public void testCustomDeliveryOverride() { + Context context = InstrumentationRegistry.getContext(); + config = BugsnagTestUtils.generateConfiguration(); + Delivery customDelivery = new Delivery() { + @Override + public void deliver(SessionTrackingPayload payload, + Configuration config) throws DeliveryFailureException {} + + @Override + public void deliver(Report report, + Configuration config) throws DeliveryFailureException {} + }; + config.setDelivery(customDelivery); + client = new Client(context, config); + assertEquals(customDelivery, client.config.getDelivery()); + } } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java index a14c781634..382caae06d 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java @@ -219,25 +219,6 @@ public void testFullManifestConfig() { assertEquals(true, newConfig.shouldAutoCaptureSessions()); } - @Test - public void testSessionTrackerApiClient() throws Exception { - Client client = new Client(InstrumentationRegistry.getContext(), "api-key"); - assertTrue(client.sessionTracker.getApiClient() instanceof DefaultHttpClient); - - SessionTrackingApiClient customClient = new SessionTrackingApiClient() { - @Override - public void postSessionTrackingPayload(String urlString, - SessionTrackingPayload payload, - Map headers) - throws NetworkException, BadResponseException { - - } - }; - client.setSessionTrackingApiClient(customClient); - assertFalse(client.sessionTracker.getApiClient() instanceof DefaultHttpClient); - assertEquals(customClient, client.sessionTracker.getApiClient()); - } - @Test public void testClientAddToTab() { Client client = generateClient(); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackerTest.java b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackerTest.java index 1eeb756fa1..4e6c7bba42 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackerTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackerTest.java @@ -31,8 +31,9 @@ public class SessionTrackerTest { @Before public void setUp() throws Exception { configuration = new Configuration("test"); - sessionTracker = new SessionTracker(configuration, generateClient(), generateSessionStore(), - generateSessionTrackingApiClient()); + configuration.setDelivery(BugsnagTestUtils.generateDelivery()); + sessionTracker + = new SessionTracker(configuration, generateClient(), generateSessionStore()); configuration.setAutoCaptureSessions(true); user = new User(); } @@ -125,7 +126,7 @@ public void testBasicInForeground() throws Exception { public void testInForegroundDuration() throws Exception { long now = System.currentTimeMillis(); sessionTracker = new SessionTracker(configuration, generateClient(), - 0, generateSessionStore(), generateSessionTrackingApiClient()); + 0, generateSessionStore()); sessionTracker.updateForegroundTracker(ACTIVITY_NAME, false, now); assertEquals(0, sessionTracker.getDurationInForegroundMs(now)); @@ -146,7 +147,7 @@ public void testInForegroundDuration() throws Exception { @Test public void testZeroSessionTimeout() throws Exception { sessionTracker = new SessionTracker(configuration, generateClient(), - 0, generateSessionStore(), generateSessionTrackingApiClient()); + 0, generateSessionStore()); long now = System.currentTimeMillis(); sessionTracker.updateForegroundTracker(ACTIVITY_NAME, true, now); @@ -161,7 +162,7 @@ public void testZeroSessionTimeout() throws Exception { @Test public void testSessionTimeout() throws Exception { sessionTracker = new SessionTracker(configuration, generateClient(), - 100, generateSessionStore(), generateSessionTrackingApiClient()); + 100, generateSessionStore()); long now = System.currentTimeMillis(); sessionTracker.updateForegroundTracker(ACTIVITY_NAME, true, now); diff --git a/sdk/src/main/java/com/bugsnag/android/Bugsnag.java b/sdk/src/main/java/com/bugsnag/android/Bugsnag.java index 59e1ea8e05..17b8d4966c 100644 --- a/sdk/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/sdk/src/main/java/com/bugsnag/android/Bugsnag.java @@ -298,7 +298,10 @@ public static void setUserName(final String name) { * https://docs.bugsnag.com/api/error-reporting/ * * @param errorReportApiClient the custom HTTP client implementation + * + * @deprecated use {@link Configuration#setDelivery(Delivery)} instead */ + @Deprecated public static void setErrorReportApiClient(@NonNull ErrorReportApiClient errorReportApiClient) { getClient().setErrorReportApiClient(errorReportApiClient); } @@ -312,11 +315,15 @@ public static void setErrorReportApiClient(@NonNull ErrorReportApiClient errorRe * the Bugsnag API. * * @param apiClient the custom HTTP client implementation + * + * @deprecated use {@link Configuration#setDelivery(Delivery)} instead */ + @Deprecated public static void setSessionTrackingApiClient(@NonNull SessionTrackingApiClient apiClient) { getClient().setSessionTrackingApiClient(apiClient); } + /** * Add a "before notify" callback, to execute code before every * report to Bugsnag. diff --git a/sdk/src/main/java/com/bugsnag/android/Client.java b/sdk/src/main/java/com/bugsnag/android/Client.java index c620678f6c..f6bc2b5406 100644 --- a/sdk/src/main/java/com/bugsnag/android/Client.java +++ b/sdk/src/main/java/com/bugsnag/android/Client.java @@ -76,8 +76,6 @@ public class Client extends Observable implements Observer { private final EventReceiver eventReceiver; final SessionTracker sessionTracker; - private ErrorReportApiClient errorReportApiClient; - private SessionTrackingApiClient sessionTrackingApiClient; /** * Initialize a Bugsnag client @@ -126,12 +124,14 @@ public Client(@NonNull Context androidContext, @NonNull Configuration configurat ConnectivityManager cm = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE); - DefaultHttpClient defaultHttpClient = new DefaultHttpClient(cm); - errorReportApiClient = defaultHttpClient; - sessionTrackingApiClient = defaultHttpClient; + + //noinspection ConstantConditions + if (configuration.getDelivery() == null) { + configuration.setDelivery(new DefaultDelivery(cm)); + } sessionTracker = - new SessionTracker(configuration, this, sessionStore, sessionTrackingApiClient); + new SessionTracker(configuration, this, sessionStore); eventReceiver = new EventReceiver(this); // Set up and collect constant app and device diagnostics @@ -164,8 +164,6 @@ public Client(@NonNull Context androidContext, @NonNull Configuration configurat + "breadcrumbs on API Levels below 14."); } - errorReportApiClient = new DefaultHttpClient(cm); - // populate from manifest (in the case where the constructor was called directly by the // User or no UUID was supplied) if (config.getBuildUUID() == null) { @@ -211,7 +209,7 @@ public void run() { // Flush any on-disk errors - errorStore.flushOnLaunch(errorReportApiClient); + errorStore.flushOnLaunch(); } private class ConnectivityChangeReceiver extends BroadcastReceiver { @@ -223,7 +221,7 @@ public void onReceive(Context context, Intent intent) { boolean retryReports = networkInfo != null && networkInfo.isConnectedOrConnecting(); if (retryReports) { - errorStore.flushAsync(errorReportApiClient); + errorStore.flushAsync(); } } } @@ -631,21 +629,36 @@ void setUserName(String name, boolean notify) { } } + DeliveryCompat getAndSetDeliveryCompat() { + Delivery current = config.getDelivery(); + + if (current instanceof DeliveryCompat) { + return (DeliveryCompat)current; + } else { + DeliveryCompat compat = new DeliveryCompat(); + config.setDelivery(compat); + return compat; + } + } + @SuppressWarnings("ConstantConditions") + @Deprecated void setErrorReportApiClient(@NonNull ErrorReportApiClient errorReportApiClient) { if (errorReportApiClient == null) { throw new IllegalArgumentException("ErrorReportApiClient cannot be null."); } - this.errorReportApiClient = errorReportApiClient; + DeliveryCompat compat = getAndSetDeliveryCompat(); + compat.errorReportApiClient = errorReportApiClient; } @SuppressWarnings("ConstantConditions") + @Deprecated void setSessionTrackingApiClient(@NonNull SessionTrackingApiClient apiClient) { if (apiClient == null) { throw new IllegalArgumentException("SessionTrackingApiClient cannot be null."); } - this.sessionTrackingApiClient = apiClient; - sessionTracker.setApiClient(apiClient); + DeliveryCompat compat = getAndSetDeliveryCompat(); + compat.sessionTrackingApiClient = apiClient; } /** @@ -914,7 +927,7 @@ public void run() { break; case ASYNC_WITH_CACHE: errorStore.write(error); - errorStore.flushAsync(errorReportApiClient); + errorStore.flushAsync(); break; default: break; @@ -1237,16 +1250,21 @@ public void disableExceptionHandler() { void deliver(@NonNull Report report, @NonNull Error error) { try { - errorReportApiClient.postReport(config.getEndpoint(), report, - config.getErrorApiHeaders()); + config.getDelivery().deliver(report, config); Logger.info("Sent 1 new error to Bugsnag"); - } catch (NetworkException exception) { - 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 (BadResponseException exception) { - Logger.info("Bad response when sending data to Bugsnag"); + } catch (DeliveryFailureException exception) { + switch (exception.reason) { + case CONNECTIVITY: + 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); + break; + case REQUEST_FAILURE: + Logger.info("Bad response when sending data to Bugsnag"); + break; + default: + break; + } } catch (Exception exception) { Logger.warn("Problem sending error to Bugsnag", exception); } diff --git a/sdk/src/main/java/com/bugsnag/android/Configuration.java b/sdk/src/main/java/com/bugsnag/android/Configuration.java index 2e84476482..e7be4effbb 100644 --- a/sdk/src/main/java/com/bugsnag/android/Configuration.java +++ b/sdk/src/main/java/com/bugsnag/android/Configuration.java @@ -8,7 +8,6 @@ import java.util.Collection; import java.util.Date; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Observable; @@ -56,6 +55,8 @@ public class Configuration extends Observable implements Observer { private String codeBundleId; private String notifierType; + private Delivery delivery; + /** * Construct a new Bugsnag configuration object * @@ -494,7 +495,42 @@ String getNotifierType() { return notifierType; } - Map getErrorApiHeaders() { + /** + * Retrieves the delivery used to make HTTP requests to Bugsnag. + * + * @return the current delivery + */ + @NonNull + public Delivery getDelivery() { + return delivery; + } + + /** + * Sets the delivery used to make HTTP requests to Bugsnag. A default implementation is + * provided, but you may wish to use your own implementation if you have requirements such + * as pinning SSL certificates, for example. + *

+ * Any custom implementation must be capable of sending + * Error Reports + * and Sessions as + * documented at https://docs.bugsnag.com/api/ + * + * @param delivery the custom HTTP client implementation + */ + public void setDelivery(@NonNull Delivery delivery) { + //noinspection ConstantConditions + if (delivery == null) { + throw new IllegalArgumentException("Delivery cannot be null"); + } + this.delivery = delivery; + } + + /** + * Supplies the headers which must be used in any request sent to the Error Reporting API. + * + * @return the HTTP headers + */ + public Map getErrorApiHeaders() { Map map = new HashMap<>(); map.put(HEADER_API_PAYLOAD_VERSION, "4.0"); map.put(HEADER_API_KEY, apiKey); @@ -502,7 +538,12 @@ Map getErrorApiHeaders() { return map; } - Map getSessionApiHeaders() { + /** + * Supplies the headers which must be used in any request sent to the Session Tracking API. + * + * @return the HTTP headers + */ + public Map getSessionApiHeaders() { Map map = new HashMap<>(); map.put(HEADER_API_PAYLOAD_VERSION, "1.0"); map.put(HEADER_API_KEY, apiKey); diff --git a/sdk/src/main/java/com/bugsnag/android/DefaultHttpClient.java b/sdk/src/main/java/com/bugsnag/android/DefaultDelivery.java similarity index 56% rename from sdk/src/main/java/com/bugsnag/android/DefaultHttpClient.java rename to sdk/src/main/java/com/bugsnag/android/DefaultDelivery.java index 348b65010b..5e5b2e2f19 100644 --- a/sdk/src/main/java/com/bugsnag/android/DefaultHttpClient.java +++ b/sdk/src/main/java/com/bugsnag/android/DefaultDelivery.java @@ -1,5 +1,8 @@ package com.bugsnag.android; +import static com.bugsnag.android.DeliveryFailureException.Reason.CONNECTIVITY; +import static com.bugsnag.android.DeliveryFailureException.Reason.REQUEST_FAILURE; + import android.net.ConnectivityManager; import android.net.NetworkInfo; @@ -10,48 +13,46 @@ import java.net.URL; import java.util.Map; -class DefaultHttpClient implements ErrorReportApiClient, SessionTrackingApiClient { +class DefaultDelivery implements Delivery { private final ConnectivityManager connectivityManager; - DefaultHttpClient(ConnectivityManager connectivityManager) { + DefaultDelivery(ConnectivityManager connectivityManager) { this.connectivityManager = connectivityManager; } @Override - public void postReport(String urlString, - Report report, - Map headers) - throws NetworkException, BadResponseException { - - int status = makeRequest(urlString, report, headers); + public void deliver(SessionTrackingPayload payload, + Configuration config) throws DeliveryFailureException { + String endpoint = config.getSessionEndpoint(); + int status = deliver(endpoint, payload, config.getSessionApiHeaders()); - if (status / 100 != 2) { - throw new BadResponseException(urlString, status); + if (status != 202) { + throw new DeliveryFailureException(REQUEST_FAILURE, + "Request failed with status " + status); } else { - Logger.info("Completed error API request"); + Logger.info("Completed session tracking request"); } } @Override - public void postSessionTrackingPayload(String urlString, - SessionTrackingPayload payload, - Map headers) - throws NetworkException, BadResponseException { - - int status = makeRequest(urlString, payload, headers); + public void deliver(Report report, + Configuration config) throws DeliveryFailureException { + String endpoint = config.getEndpoint(); + int status = deliver(endpoint, report, config.getErrorApiHeaders()); - if (status != 202) { - throw new BadResponseException(urlString, status); + if (status / 100 != 2) { + throw new DeliveryFailureException(REQUEST_FAILURE, + "Request failed with status " + status); } else { - Logger.info("Completed session tracking request"); + Logger.info("Completed error API request"); } } - private int makeRequest(String urlString, - JsonStream.Streamable streamable, - Map headers) throws NetworkException { - checkHasNetworkConnection(urlString); + int deliver(String urlString, + JsonStream.Streamable streamable, + Map headers) throws DeliveryFailureException { + checkHasNetworkConnection(); HttpURLConnection conn = null; try { @@ -76,24 +77,22 @@ private int makeRequest(String urlString, IOUtils.closeQuietly(out); } - // End the request, get the response code return conn.getResponseCode(); } catch (IOException exception) { - throw new NetworkException(urlString, exception); + throw new DeliveryFailureException(CONNECTIVITY, + "IOException encountered in request", exception); } finally { IOUtils.close(conn); } } - private void checkHasNetworkConnection(String urlString) throws NetworkException { + private void checkHasNetworkConnection() throws DeliveryFailureException { NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); // conserve device battery by avoiding radio use if (!(activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting())) { - RuntimeException rex = new RuntimeException("No network connection available"); - throw new NetworkException(urlString, rex); + throw new DeliveryFailureException(CONNECTIVITY, "No network connection available"); } } - } diff --git a/sdk/src/main/java/com/bugsnag/android/Delivery.java b/sdk/src/main/java/com/bugsnag/android/Delivery.java new file mode 100644 index 0000000000..de15adf3cb --- /dev/null +++ b/sdk/src/main/java/com/bugsnag/android/Delivery.java @@ -0,0 +1,69 @@ +package com.bugsnag.android; + + +/** + * Implementations of this interface deliver Error Reports and Sessions captured to the Bugsnag API. + *

+ * A default {@link Delivery} implementation is provided as part of Bugsnag initialization, + * but you may wish to use your own implementation if you have requirements such + * as pinning SSL certificates, for example. + *

+ * Any custom implementation must be capable of sending + * + * Error Reports and Sessions as + * documented at https://docs.bugsnag.com/api/ + * + * @see DefaultDelivery + */ +public interface Delivery { + + /** + * Posts an array of sessions to the Bugsnag Session Tracking API. + *

+ * This request must be delivered to the endpoint at {@link Configuration#getSessionEndpoint()}, + * and contain the HTTP headers from {@link Configuration#getSessionApiHeaders()}. + *

+ * If the response status code is not 202, then the implementation must throw + * {@link DeliveryFailureException} with a reason of + * {@link DeliveryFailureException.Reason#REQUEST_FAILURE}. + *

+ * If the request could not be delivered due to connectivity issues, then the implementation + * must throw {@link DeliveryFailureException} with a reason of + * {@link DeliveryFailureException.Reason#CONNECTIVITY}, as this will cache the request for + * delivery at a future time. + *

+ * See + * https://docs.bugsnag.com/api/sessions/ + * + * @param payload The session tracking payload + * @param config The configuration by which this request will be sent + * @throws DeliveryFailureException when delivery does not receive a 202 status code. + */ + void deliver(SessionTrackingPayload payload, + Configuration config) throws DeliveryFailureException; + + /** + * Posts an Error Report to the Bugsnag Error Reporting API. + *

+ * This request must be delivered to the endpoint at {@link Configuration#getSessionEndpoint()}, + * and contain the HTTP headers from {@link Configuration#getSessionApiHeaders()}. + *

+ * If the response status code is not 2xx, then the implementation must throw + * {@link DeliveryFailureException} with a reason of + * {@link DeliveryFailureException.Reason#REQUEST_FAILURE}. + *

+ * If the request could not be delivered due to connectivity issues, then the implementation + * must throw {@link DeliveryFailureException} with a reason of + * {@link DeliveryFailureException.Reason#CONNECTIVITY}, as this will cache the request for + * delivery at a future time. + *

+ * See + * https://docs.bugsnag.com/api/error-reporting/ + * + * @param report The error report + * @param config The configuration by which this request will be sent + * @throws DeliveryFailureException when delivery does not receive a 2xx status code. + */ + void deliver(Report report, + Configuration config) throws DeliveryFailureException; +} diff --git a/sdk/src/main/java/com/bugsnag/android/DeliveryCompat.java b/sdk/src/main/java/com/bugsnag/android/DeliveryCompat.java new file mode 100644 index 0000000000..e27bc809d0 --- /dev/null +++ b/sdk/src/main/java/com/bugsnag/android/DeliveryCompat.java @@ -0,0 +1,51 @@ +package com.bugsnag.android; + +import static com.bugsnag.android.DeliveryFailureException.Reason.CONNECTIVITY; +import static com.bugsnag.android.DeliveryFailureException.Reason.REQUEST_FAILURE; + +/** + * A compatibility implementation of {@link Delivery} which wraps {@link ErrorReportApiClient} and + * {@link SessionTrackingApiClient}. This class allows for backwards compatibility for users still + * utilising the old API, and should be removed in the next major version. + */ +class DeliveryCompat implements Delivery { + + volatile ErrorReportApiClient errorReportApiClient; + volatile SessionTrackingApiClient sessionTrackingApiClient; + + @Override + public void deliver(SessionTrackingPayload payload, + Configuration config) throws DeliveryFailureException { + if (sessionTrackingApiClient != null) { + + try { + sessionTrackingApiClient.postSessionTrackingPayload(config.getSessionEndpoint(), + payload, config.getSessionApiHeaders()); + } catch (NetworkException | BadResponseException exception) { + throw convertException(exception); + } + } + } + + @Override + public void deliver(Report report, Configuration config) throws DeliveryFailureException { + if (errorReportApiClient != null) { + try { + errorReportApiClient.postReport(config.getEndpoint(), + report, config.getErrorApiHeaders()); + } catch (NetworkException | BadResponseException exception) { + throw convertException(exception); + } + } + } + + DeliveryFailureException convertException(Exception exception) { + if (exception instanceof NetworkException) { + return new DeliveryFailureException(CONNECTIVITY, exception.getMessage(), exception); + } else if (exception instanceof BadResponseException) { + return new DeliveryFailureException(REQUEST_FAILURE, exception.getMessage(), exception); + } else { + return null; + } + } +} diff --git a/sdk/src/main/java/com/bugsnag/android/DeliveryFailureException.java b/sdk/src/main/java/com/bugsnag/android/DeliveryFailureException.java new file mode 100644 index 0000000000..d0a3321395 --- /dev/null +++ b/sdk/src/main/java/com/bugsnag/android/DeliveryFailureException.java @@ -0,0 +1,21 @@ +package com.bugsnag.android; + +public class DeliveryFailureException extends Exception { + + enum Reason { + CONNECTIVITY, + REQUEST_FAILURE + } + + final Reason reason; + + public DeliveryFailureException(Reason reason, String msg) { + super(msg); + this.reason = reason; + } + + public DeliveryFailureException(Reason reason, String msg, Throwable throwable) { + super(msg, throwable); + this.reason = reason; + } +} diff --git a/sdk/src/main/java/com/bugsnag/android/ErrorReportApiClient.java b/sdk/src/main/java/com/bugsnag/android/ErrorReportApiClient.java index a67b05c43b..1c21bbb4eb 100644 --- a/sdk/src/main/java/com/bugsnag/android/ErrorReportApiClient.java +++ b/sdk/src/main/java/com/bugsnag/android/ErrorReportApiClient.java @@ -7,9 +7,10 @@ * place of the default implementation, by calling * {@link Bugsnag#setErrorReportApiClient(ErrorReportApiClient)} * - * @see DefaultHttpClient + * @deprecated use {@link Delivery} to send error reports */ @SuppressWarnings("WeakerAccess") +@Deprecated public interface ErrorReportApiClient { /** diff --git a/sdk/src/main/java/com/bugsnag/android/ErrorStore.java b/sdk/src/main/java/com/bugsnag/android/ErrorStore.java index c6f32711e4..aea0e50f18 100644 --- a/sdk/src/main/java/com/bugsnag/android/ErrorStore.java +++ b/sdk/src/main/java/com/bugsnag/android/ErrorStore.java @@ -50,11 +50,11 @@ public int compare(File lhs, File rhs) { "/bugsnag-errors/", 128, ERROR_REPORT_COMPARATOR); } - void flushOnLaunch(final ErrorReportApiClient errorReportApiClient) { + void flushOnLaunch() { final List crashReports = findLaunchCrashReports(); if (crashReports.isEmpty() || config.getLaunchCrashThresholdMs() == 0) { - flushAsync(errorReportApiClient); // if disabled or no startup crash, flush async + flushAsync(); // if disabled or no startup crash, flush async } else { // Block the main thread for a 2 second interval as the app may crash very soon. @@ -66,7 +66,7 @@ void flushOnLaunch(final ErrorReportApiClient errorReportApiClient) { Async.run(new Runnable() { @Override public void run() { - flushReports(crashReports, errorReportApiClient); + flushReports(crashReports); flushOnLaunchCompleted = true; } }); @@ -88,7 +88,7 @@ public void run() { /** * Flush any on-disk errors to Bugsnag */ - void flushAsync(final ErrorReportApiClient errorReportApiClient) { + void flushAsync() { if (storeDirectory == null) { return; } @@ -97,7 +97,7 @@ void flushAsync(final ErrorReportApiClient errorReportApiClient) { Async.run(new Runnable() { @Override public void run() { - flushReports(findStoredFiles(), errorReportApiClient); + flushReports(findStoredFiles()); } }); } catch (RejectedExecutionException exception) { @@ -105,15 +105,14 @@ public void run() { } } - private void flushReports(Collection storedReports, - ErrorReportApiClient apiClient) { + private void flushReports(Collection storedReports) { if (!storedReports.isEmpty() && semaphore.tryAcquire(1)) { try { Logger.info(String.format(Locale.US, "Sending %d saved error(s) to Bugsnag", storedReports.size())); for (File errorFile : storedReports) { - flushErrorReport(errorFile, apiClient); + flushErrorReport(errorFile); } } finally { semaphore.release(1); @@ -121,24 +120,36 @@ private void flushReports(Collection storedReports, } } - private void flushErrorReport(File errorFile, ErrorReportApiClient errorReportApiClient) { + private void flushErrorReport(File errorFile) { try { Report report = new Report(config.getApiKey(), errorFile); - errorReportApiClient.postReport(config.getEndpoint(), report, - config.getErrorApiHeaders()); + config.getDelivery().deliver(report, config); deleteStoredFiles(Collections.singleton(errorFile)); Logger.info("Deleting sent error file " + errorFile.getName()); - } catch (NetworkException exception) { - cancelQueuedFiles(Collections.singleton(errorFile)); - Logger.warn("Could not send previously saved error(s)" - + " to Bugsnag, will try again later", exception); + } catch (DeliveryFailureException exception) { + switch (exception.reason) { + case CONNECTIVITY: + cancelQueuedFiles(Collections.singleton(errorFile)); + Logger.warn("Could not send previously saved error(s)" + + " to Bugsnag, will try again later", exception); + break; + case REQUEST_FAILURE: + handleRequestFailure(errorFile, exception); + break; + default: + break; + } } catch (Exception exception) { - deleteStoredFiles(Collections.singleton(errorFile)); - Logger.warn("Problem sending unsent error from disk", exception); + handleRequestFailure(errorFile, exception); } } + private void handleRequestFailure(File errorFile, Exception exception) { + deleteStoredFiles(Collections.singleton(errorFile)); + Logger.warn("Problem sending unsent error from disk", exception); + } + boolean isLaunchCrashReport(File file) { return file.getName().endsWith("_startupcrash.json"); } diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java index b9d797be07..35a217f739 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java @@ -30,7 +30,6 @@ class SessionTracker implements Application.ActivityLifecycleCallbacks { private final long timeoutMs; private final Client client; private final SessionStore sessionStore; - private SessionTrackingApiClient apiClient; // This most recent time an Activity was stopped. private AtomicLong activityLastStoppedAtMs = new AtomicLong(0); @@ -40,18 +39,16 @@ class SessionTracker implements Application.ActivityLifecycleCallbacks { private AtomicReference currentSession = new AtomicReference<>(); private Semaphore flushingRequest = new Semaphore(1); - SessionTracker(Configuration configuration, Client client, SessionStore sessionStore, - SessionTrackingApiClient apiClient) { - this(configuration, client, DEFAULT_TIMEOUT_MS, sessionStore, apiClient); + SessionTracker(Configuration configuration, Client client, SessionStore sessionStore) { + this(configuration, client, DEFAULT_TIMEOUT_MS, sessionStore); } SessionTracker(Configuration configuration, Client client, long timeoutMs, - SessionStore sessionStore, SessionTrackingApiClient apiClient) { + SessionStore sessionStore) { this.configuration = configuration; this.client = client; this.timeoutMs = timeoutMs; this.sessionStore = sessionStore; - this.apiClient = apiClient; } /** @@ -69,14 +66,6 @@ void startNewSession(@NonNull Date date, @Nullable User user, boolean autoCaptur trackSessionIfNeeded(session); } - void setApiClient(SessionTrackingApiClient apiClient) { - this.apiClient = apiClient; - } - - SessionTrackingApiClient getApiClient() { - return apiClient; - } - /** * Determines whether or not a session should be tracked. If this is true, the session will be * stored and sent to the Bugsnag API, otherwise no action will occur in this method. @@ -101,13 +90,19 @@ public void run() { new SessionTrackingPayload(session, client.appData); try { - apiClient.postSessionTrackingPayload(endpoint, payload, - configuration.getSessionApiHeaders()); - } catch (NetworkException exception) { // store for later sending - Logger.info("Failed to post session payload"); - sessionStore.write(session); - } catch (BadResponseException exception) { // drop bad data - Logger.warn("Invalid session tracking payload", exception); + configuration.getDelivery().deliver(payload, configuration); + } catch (DeliveryFailureException exception) { // store for later sending + switch (exception.reason) { + case CONNECTIVITY: + Logger.info("Failed to post session payload"); + sessionStore.write(session); + break; + case REQUEST_FAILURE: + Logger.warn("Invalid session tracking payload", exception); + break; + default: + break; + } } } }); @@ -168,16 +163,22 @@ void flushStoredSessions() { //FUTURE:SM Reduce duplication here and above try { - final String endpoint = configuration.getSessionEndpoint(); - apiClient.postSessionTrackingPayload(endpoint, payload, - configuration.getSessionApiHeaders()); - sessionStore.deleteStoredFiles(storedFiles); - } catch (NetworkException exception) { // store for later sending - sessionStore.cancelQueuedFiles(storedFiles); - Logger.info("Failed to post stored session payload"); - } catch (BadResponseException exception) { // drop bad data - Logger.warn("Invalid session tracking payload", exception); + configuration.getDelivery().deliver(payload, configuration); sessionStore.deleteStoredFiles(storedFiles); + } catch (DeliveryFailureException exception) { + switch (exception.reason) { + case CONNECTIVITY: // store for later sending + sessionStore.cancelQueuedFiles(storedFiles); + Logger.info("Failed to post stored session payload"); + break; + case REQUEST_FAILURE: + // drop bad data + Logger.warn("Invalid session tracking payload", exception); + sessionStore.deleteStoredFiles(storedFiles); + break; + default: + break; + } } } } finally { diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTrackingApiClient.java b/sdk/src/main/java/com/bugsnag/android/SessionTrackingApiClient.java index f82f05288f..cabd2d6b23 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTrackingApiClient.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTrackingApiClient.java @@ -7,8 +7,9 @@ * of this client can be used in place of the default implementation, by calling * {@link Bugsnag#setSessionTrackingApiClient(SessionTrackingApiClient)} * - * @see DefaultHttpClient + * @deprecated use {@link Delivery} to send sessions */ +@Deprecated public interface SessionTrackingApiClient { /**