Skip to content

Commit

Permalink
MBL-2054: Manage Exceptions from Rx streams lacking onError #2216 Open
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyteate authored Jan 30, 2025
1 parent b317991 commit d6261ed
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 7 deletions.
26 changes: 19 additions & 7 deletions app/src/main/java/com/kickstarter/KSApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.text.TextUtils;

import com.apollographql.apollo3.exception.ApolloHttpException;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import com.kickstarter.libs.ApiEndpoint;
import com.kickstarter.libs.FirebaseHelper;
Expand All @@ -26,6 +27,7 @@
import androidx.multidex.MultiDex;
import androidx.multidex.MultiDexApplication;

import io.reactivex.exceptions.OnErrorNotImplementedException;
import io.reactivex.exceptions.UndeliverableException;
import io.reactivex.plugins.RxJavaPlugins;
import timber.log.Timber;
Expand Down Expand Up @@ -116,15 +118,25 @@ private void setVisitorCookie() {
}

private void createErrorHandler() {
RxJavaPlugins.setErrorHandler(e -> {
if (e instanceof UndeliverableException) {
Timber.w(e, "Undeliverable Exception");
if (e.getMessage() != null) {
FirebaseCrashlytics.getInstance().setCustomKey("Undeliverable Exception", e.getMessage());
RxJavaPlugins.setErrorHandler(t -> {
if (t instanceof OnErrorNotImplementedException
&& t.getCause() != null
&& t.getCause() instanceof ApolloHttpException
&& ((ApolloHttpException) t.getCause()).getStatusCode() == 429) {
Timber.e(t, "RxJavaPlugins.setErrorHandler");
final ApolloHttpException apolloHttpException = (ApolloHttpException) t.getCause();
final String value = apolloHttpException.getMessage() != null ? apolloHttpException.getMessage() : "";
FirebaseCrashlytics.getInstance().setCustomKey("ApolloHttpException (429)", value);
FirebaseCrashlytics.getInstance().recordException(t);
} else if (t instanceof UndeliverableException) {
Timber.w(t, "Undeliverable Exception");
if (t.getMessage() != null) {
FirebaseCrashlytics.getInstance().setCustomKey("Undeliverable Exception", t.getMessage());
}
FirebaseCrashlytics.getInstance().recordException(e);
FirebaseCrashlytics.getInstance().recordException(t);
} else {
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
Timber.e(t, "RxJavaPlugins.setErrorHandler");
FirebaseCrashlytics.getInstance().recordException(t);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.kickstarter.libs.featureflag.FlagKey
import com.kickstarter.libs.rx.transformers.Transformers
import com.kickstarter.libs.utils.EventName
import com.kickstarter.libs.utils.extensions.addToDisposable
import com.kickstarter.libs.utils.extensions.fromJson
import com.kickstarter.libs.utils.extensions.positionFromSort
import com.kickstarter.mock.MockFeatureFlagClient
import com.kickstarter.mock.factories.ApiExceptionFactory
Expand All @@ -34,6 +35,7 @@ import com.kickstarter.ui.viewholders.discoverydrawer.LoggedOutViewHolder
import com.kickstarter.ui.viewholders.discoverydrawer.TopFilterViewHolder
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.subscribers.TestSubscriber
import org.junit.After
import org.junit.Test
Expand Down Expand Up @@ -67,10 +69,48 @@ class DiscoveryViewModelTest : KSRobolectricTestCase() {
private val showNotifPermissionRequest = TestSubscriber<Unit>()
private val showConsentManagementDialog = TestSubscriber<Unit>()
private val darkThemeEnabled = TestSubscriber<Boolean>()
private val exceptions = TestSubscriber<Throwable>()
private val disposables = CompositeDisposable()
private fun setUpEnvironment(environment: Environment) {
vm = DiscoveryViewModel.DiscoveryViewModel(environment)
}
// In the future, this can be dynamic and moved to the base class
private fun setUpRxJavaPluginsErrorHandler() {
RxJavaPlugins.setErrorHandler { t: Throwable ->
exceptions.onNext(t)
}
}
private fun tearDownRxJavaPluginsErrorHandler() {
RxJavaPlugins.setErrorHandler(null)
}

@Test
fun `test null Intent without error handling`() {
setUpRxJavaPluginsErrorHandler()

// Emulate V1 API deserialization of a response that does not adhere to expected schema
val emailVerificationEnvelope = environment().gson()?.fromJson<EmailVerificationEnvelope>("{}")!!

val url = "https://*.kickstarter.com/profile/verify_email"
val intentWithUrl = Intent().setData(Uri.parse(url))
val mockApiClient: MockApiClientV2 = object : MockApiClientV2() {
override fun verifyEmail(token: String): Observable<EmailVerificationEnvelope> {
return Observable.just(emailVerificationEnvelope)
}
}
val mockedClientEnvironment = environment().toBuilder()
.apiClientV2(mockApiClient)
.build()
setUpEnvironment(mockedClientEnvironment)

vm.outputs.showSuccessMessage().subscribe { showSuccessMessage.onNext(it) }.addToDisposable(disposables)
vm.provideIntent(intentWithUrl)
showSuccessMessage.assertNoValues()

exceptions.assertValueCount(1)

tearDownRxJavaPluginsErrorHandler()
}

@Test
fun `test Dark Mode disabled`() {
Expand Down

0 comments on commit d6261ed

Please sign in to comment.