├── docs ├── images │ └── uber_oss_logo.png ├── fonts │ └── UberMove-Medium.woff2 ├── under-the-hood.md ├── shrinking.md ├── css │ └── app.css ├── benchmark.md └── examples.md ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── publish-docs.gradle └── dependencies.gradle ├── rxdogtag-autodispose ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── proguard │ │ │ │ └── rxdogtag-autodispose.pro │ │ └── java │ │ │ └── rxdogtag2 │ │ │ └── autodispose2 │ │ │ ├── AutoDisposeConfigurer.java │ │ │ └── AutoDisposeObserverHandler.java │ └── test │ │ └── java │ │ └── anotherpackage │ │ └── AutoDisposeObserverHandlerTest.java ├── gradle.properties └── build.gradle ├── rxdogtag ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── proguard │ │ │ │ └── rxdogtag.pro │ │ └── java │ │ │ └── rxdogtag2 │ │ │ ├── RxDogTagErrorReceiver.java │ │ │ ├── RxDogTagTaggedExceptionReceiver.java │ │ │ ├── DogTagCompletableObserver.java │ │ │ ├── DogTagSingleObserver.java │ │ │ ├── DogTagObserver.java │ │ │ ├── ObserverHandler.java │ │ │ ├── DogTagMaybeObserver.java │ │ │ └── DogTagSubscriber.java │ ├── jmh │ │ └── java │ │ │ └── rxdogtag2 │ │ │ ├── util │ │ │ ├── PerfConsumer.java │ │ │ └── PerfObserver.java │ │ │ ├── EventHandlingPerf.java │ │ │ ├── SubscriptionPerf.java │ │ │ └── EntireProcessPerf.java │ └── test │ │ └── java │ │ └── anotherpackage │ │ ├── DogTagTestUtil.java │ │ ├── EmptySingleObserver.java │ │ ├── EmptyCompletableObserver.java │ │ ├── EmptyObserver.java │ │ ├── EmptyMaybeObserver.java │ │ ├── EmptySubscriber.java │ │ ├── DogTagTest.java │ │ ├── ReadMeExample.java │ │ ├── RxErrorsRule.java │ │ ├── AggressiveUncaughtExceptionHandlerRule.java │ │ ├── ObserverHandlerDefaultsTest.java │ │ ├── RxDogTagConfigurationTest.java │ │ ├── DogTagObserverTest.java │ │ └── DogTagObserverDelegatesTest.java ├── gradle.properties └── build.gradle ├── spotless └── copyright.java ├── android-benchmark └── benchmark │ ├── gradle.properties │ ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── rxdogtag2 │ │ │ └── androidbenchmark │ │ │ └── DataParser.kt │ └── androidTest │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── rxdogtag2 │ │ └── androidbenchmark │ │ └── RxDogTagAndroidPerf.kt │ └── build.gradle ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── RELEASING.md ├── gradle.properties ├── deploy_website.sh ├── mkdocs.yml ├── settings.gradle ├── .gitignore ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── gradlew.bat ├── gradlew ├── CHANGELOG.md ├── README.md └── LICENSE.txt /docs/images/uber_oss_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/RxDogTag/HEAD/docs/images/uber_oss_logo.png -------------------------------------------------------------------------------- /docs/fonts/UberMove-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/RxDogTag/HEAD/docs/fonts/UberMove-Medium.woff2 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/RxDogTag/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /rxdogtag-autodispose/src/main/resources/META-INF/proguard/rxdogtag-autodispose.pro: -------------------------------------------------------------------------------- 1 | # We keep these in order for DogTagObservers to properly work and resolve entries in each of these packages 2 | -keepnames class autodispose2.** -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-rc-1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /docs/under-the-hood.md: -------------------------------------------------------------------------------- 1 | ## Under the hood 2 | 3 | More information on `LambdaConsumerIntrospection` can be found in its implementation PR: https://github.com/ReactiveX/RxJava/pull/5590 4 | 5 | Decision tree: 6 | ``` 7 | -> Is the observer an instance of `LambdaConsumerIntrospection`? 8 | -> Does `hasCustomOnError()` return `false`? 9 | -> Decorate with a DogTag observer. 10 | ``` -------------------------------------------------------------------------------- /rxdogtag/src/main/resources/META-INF/proguard/rxdogtag.pro: -------------------------------------------------------------------------------- 1 | # We keep these in order for DogTagObservers to properly work and resolve entries in each of these packages 2 | -keepnames class io.reactivex.rxjava3** 3 | 4 | # R8 may inline subscribe() calls entirely to their call-sites, which breaks RxDogTag's tagging 5 | # While this is sort of a cardinal sin of libraries to do this, RxDogTag is only a few classes and 6 | # incredibly small, so we claim this is a small price to pay. 7 | -keep class rxdogtag2.** { *; } 8 | -keepclassmembers class io.reactivex.rxjava3.** { 9 | *** subscribe(...); 10 | } 11 | -------------------------------------------------------------------------------- /docs/shrinking.md: -------------------------------------------------------------------------------- 1 | ## Proguard/R8 2 | 3 | In order to correctly identify which line is the `subscribe()` line, RxDogTag requires keeping the names of 4 | any types in `io.reactivex` or `com.uber.rxdogtag` packages. This doesn't prevent shrinking unused 5 | types, and as these are open source projects we don't think keeping these public risks any sort of 6 | secret sauce. Keeping names has a negligible impact on APK size as well. If this is an issue for you 7 | though, then RxDogTag is probably not the right solution for your project. 8 | 9 | RxDogTag ships with custom `rxdogtag.pro` rules in the jar resources to handle this automatically. -------------------------------------------------------------------------------- /docs/css/app.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: uber-move; 3 | src: url("../fonts/UberMove-Medium.woff2") format("woff2"); 4 | font-weight: 400; 5 | font-style: normal 6 | } 7 | 8 | @font-face { 9 | font-family: uber-move; 10 | src: url("../fonts/UberMove-Medium.woff2") format("woff2"); 11 | font-weight: 500; 12 | font-style: normal 13 | } 14 | 15 | @font-face { 16 | font-family: uber-move; 17 | src: url("../fonts/UberMove-Medium.woff2") format("woff2"); 18 | font-weight: 700; 19 | font-style: normal 20 | } 21 | 22 | body, input { 23 | font-family: uber-move,"Helvetica Neue",helvetica,sans-serif; 24 | } 25 | -------------------------------------------------------------------------------- /spotless/copyright.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) $YEAR. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /android-benchmark/benchmark/gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019. Uber Technologies 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | android.enableAdditionalTestOutput=true 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | **Library version**: 9 | 10 | 11 | **Repro steps or stacktrace**: 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | **Description**: 9 | 10 | 11 | **Related issue(s)**: 12 | 13 | 14 | -------------------------------------------------------------------------------- /rxdogtag/gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019. Uber Technologies 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | POM_NAME=RxDogTag 18 | POM_ARTIFACT_ID=rxdogtag 19 | POM_PACKAGING=jar 20 | AUTOMATIC_MODULE_NAME=rxdogtag2 21 | -------------------------------------------------------------------------------- /rxdogtag-autodispose/gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019. Uber Technologies 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | POM_NAME=RxDogTag (AutoDispose Support) 18 | POM_ARTIFACT_ID=rxdogtag-autodispose 19 | POM_PACKAGING=jar 20 | AUTOMATIC_MODULE_NAME=rxdogtag2.autodispose2 21 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ========= 3 | 4 | 1. Change the version in `gradle.properties` to a non-SNAPSHOT version. 5 | 2. Update the `CHANGELOG.md` for the impending release. 6 | 3. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version) 7 | 4. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version) 8 | 5. `./gradlew clean uploadArchives --no-parallel` 9 | 6. Update the `gradle.properties` to the next SNAPSHOT version. 10 | 7. `git commit -am "Prepare next development version."` 11 | 8. `git push && git push --tags` 12 | 9. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact. 13 | - Select the artifact, click `close`, wait for it to close, then select again and click 14 | `release`. 15 | 10. After release propagates (wait ~1 hour), update Javadocs/KDocs via [Osstrich](https://github.com/square/osstrich) 16 | - Make sure you have push access 17 | - `./gradlew publishDocs` 18 | -------------------------------------------------------------------------------- /android-benchmark/benchmark/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /gradle/publish-docs.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | configurations { 18 | osstrich 19 | } 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | osstrich 'com.squareup.osstrich:osstrich:1.4.0' 27 | } 28 | 29 | task publishDocs(type: JavaExec) { 30 | classpath = configurations.osstrich 31 | main = 'com.squareup.osstrich.JavadocPublisher' 32 | args = ['build/javadoc', 33 | 'https://github.com/uber/rxdogtag', 34 | 'com.uber.rxdogtag'] 35 | } 36 | -------------------------------------------------------------------------------- /rxdogtag/src/jmh/java/rxdogtag2/util/PerfConsumer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2.util; 17 | 18 | import io.reactivex.rxjava3.functions.Consumer; 19 | import org.openjdk.jmh.infra.Blackhole; 20 | 21 | public final class PerfConsumer implements Consumer { 22 | 23 | final Blackhole bh; 24 | 25 | public PerfConsumer(Blackhole bh) { 26 | this.bh = bh; 27 | } 28 | 29 | @Override 30 | public void accept(Object o) throws Exception { 31 | bh.consume(o); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /android-benchmark/benchmark/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /rxdogtag/src/main/java/rxdogtag2/RxDogTagErrorReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import io.reactivex.rxjava3.annotations.NonNull; 19 | 20 | /** 21 | * A marker type to indicate that RxDogTag's decorating observers should try the onError() of the 22 | * delegate of the observer that implements this. 23 | */ 24 | public interface RxDogTagErrorReceiver { 25 | /** 26 | * Called once if the deferred computation 'throws' an exception. 27 | * 28 | * @param e the exception, not null. 29 | */ 30 | void onError(@NonNull Throwable e); 31 | } 32 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/DogTagTestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | final class DogTagTestUtil { 19 | 20 | /** 21 | * Get the current line number. 22 | * 23 | * @return int - Current line number. 24 | */ 25 | static int getLineNumber() { 26 | return Thread.currentThread().getStackTrace()[2].getLineNumber(); 27 | } 28 | 29 | /** 30 | * Get the previous line number. 31 | * 32 | * @return int - Previous line number. 33 | */ 34 | static int getPreviousLineNumber() { 35 | return Thread.currentThread().getStackTrace()[2].getLineNumber() - 1; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019. Uber Technologies 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | GROUP=com.uber.rxdogtag2 17 | VERSION_NAME=2.1.0-SNAPSHOT 18 | POM_DESCRIPTION=Automatic tagging of RxJava 2 originating subscribe points for onError() investigation. 19 | POM_URL=https://github.com/uber/RxDogTag/ 20 | POM_SCM_URL=https://github.com/uber/RxDogTag/ 21 | POM_SCM_CONNECTION=scm:git:git://github.com/uber/RxDogTag.git 22 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/uber/RxDogTag.git 23 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 24 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 25 | POM_LICENCE_DIST=repo 26 | POM_DEVELOPER_ID=uber 27 | POM_DEVELOPER_NAME=Uber Technologies 28 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/EmptySingleObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import io.reactivex.rxjava3.core.SingleObserver; 19 | import io.reactivex.rxjava3.disposables.Disposable; 20 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 21 | 22 | class EmptySingleObserver implements SingleObserver, LambdaConsumerIntrospection { 23 | 24 | @Override 25 | public void onSubscribe(Disposable d) {} 26 | 27 | @Override 28 | public void onSuccess(T t) {} 29 | 30 | @Override 31 | public final void onError(Throwable e) {} 32 | 33 | @Override 34 | public final boolean hasCustomOnError() { 35 | return false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/EmptyCompletableObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import io.reactivex.rxjava3.core.CompletableObserver; 19 | import io.reactivex.rxjava3.disposables.Disposable; 20 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 21 | 22 | class EmptyCompletableObserver implements CompletableObserver, LambdaConsumerIntrospection { 23 | 24 | @Override 25 | public void onSubscribe(Disposable d) {} 26 | 27 | @Override 28 | public void onComplete() {} 29 | 30 | @Override 31 | public final void onError(Throwable e) {} 32 | 33 | @Override 34 | public boolean hasCustomOnError() { 35 | return false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/EmptyObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import io.reactivex.rxjava3.core.Observer; 19 | import io.reactivex.rxjava3.disposables.Disposable; 20 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 21 | 22 | class EmptyObserver implements Observer, LambdaConsumerIntrospection { 23 | 24 | @Override 25 | public void onSubscribe(Disposable d) {} 26 | 27 | @Override 28 | public void onNext(T t) {} 29 | 30 | @Override 31 | public final void onError(Throwable e) {} 32 | 33 | @Override 34 | public void onComplete() {} 35 | 36 | @Override 37 | public final boolean hasCustomOnError() { 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/EmptyMaybeObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import io.reactivex.rxjava3.core.MaybeObserver; 19 | import io.reactivex.rxjava3.disposables.Disposable; 20 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 21 | 22 | class EmptyMaybeObserver implements MaybeObserver, LambdaConsumerIntrospection { 23 | 24 | @Override 25 | public void onSubscribe(Disposable d) {} 26 | 27 | @Override 28 | public void onSuccess(T t) {} 29 | 30 | @Override 31 | public final void onError(Throwable e) {} 32 | 33 | @Override 34 | public void onComplete() {} 35 | 36 | @Override 37 | public final boolean hasCustomOnError() { 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: JDK ${{ matrix.java_version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | java_version: [1.8,10,11,12,13,14] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v1 15 | - name: Install JDK 16 | uses: actions/setup-java@v1 17 | with: 18 | java-version: ${{ matrix.java_version }} 19 | - name: Gradle Wrapper Validation 20 | uses: gradle/wrapper-validation-action@v1 21 | - name: Configure Gradle 22 | # Initial gradle configuration, install dependencies, etc 23 | run: ./gradlew help 24 | - name: Spot check 25 | # Run spotless first to fail fast on spotless issues 26 | run: ./gradlew spotlessCheck --stacktrace 27 | - name: Build project 28 | run: ./gradlew assemble --stacktrace 29 | - name: Run tests 30 | run: ./gradlew test --stacktrace 31 | - name: Final checks 32 | run: ./gradlew check --stacktrace 33 | - name: Upload snapshot (main only) 34 | run: ./gradlew uploadArchives 35 | env: 36 | SONATYPE_NEXUS_USERNAME: ${{ secrets.SonatypeUsername }} 37 | SONATYPE_NEXUS_PASSWORD: ${{ secrets.SonatypePassword }} 38 | if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && matrix.java_version == '1.8' 39 | -------------------------------------------------------------------------------- /rxdogtag/src/jmh/java/rxdogtag2/util/PerfObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2.util; 17 | 18 | import io.reactivex.rxjava3.core.Observer; 19 | import io.reactivex.rxjava3.disposables.Disposable; 20 | import org.openjdk.jmh.infra.Blackhole; 21 | 22 | public final class PerfObserver implements Observer { 23 | final Blackhole bh; 24 | 25 | public PerfObserver(Blackhole bh) { 26 | this.bh = bh; 27 | } 28 | 29 | @Override 30 | public void onSubscribe(Disposable d) { 31 | bh.consume(d); 32 | } 33 | 34 | @Override 35 | public void onNext(Object value) { 36 | bh.consume(value); 37 | } 38 | 39 | @Override 40 | public void onError(Throwable e) { 41 | bh.consume(e); 42 | } 43 | 44 | @Override 45 | public void onComplete() {} 46 | } 47 | -------------------------------------------------------------------------------- /deploy_website.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The website is built using MkDocs with the Material theme. 4 | # https://squidfunk.github.io/mkdocs-material/ 5 | # It requires Python to run. 6 | # Install the packages with the following command: 7 | # pip install mkdocs mkdocs-material 8 | 9 | if [ "$1" = "--local" ]; then local=true; fi 10 | if ! [ $local ]; then 11 | set -ex 12 | 13 | REPO="git@github.com:uber/RxDogTag.git" 14 | DIR=temp-clone 15 | 16 | # Delete any existing temporary website clone 17 | rm -rf $DIR 18 | 19 | # Clone the current repo into temp folder 20 | git clone $REPO $DIR 21 | 22 | # Move working directory into temp folder 23 | cd $DIR 24 | 25 | # Fetch all tags 26 | git fetch --all --tags --prune 27 | 28 | # Checkout the last 1.x release 29 | git checkout tags/1.0.0 30 | 31 | # Generate the 1.x docs 32 | ./gradlew dokka 33 | 34 | git checkout main 35 | 36 | # Generate docs for 2.x 37 | ./gradlew dokka 38 | fi 39 | 40 | # Copy in special files that GitHub wants in the project root. 41 | cat README.md > docs/index.md 42 | cp CHANGELOG.md docs/changelog.md 43 | cp CONTRIBUTING.md docs/contributing.md 44 | cp CODE_OF_CONDUCT.md docs/code-of-conduct.md 45 | 46 | # Build the site and push the new files up to GitHub 47 | if ! [ $local ]; then 48 | mkdocs gh-deploy 49 | else 50 | mkdocs serve 51 | fi 52 | 53 | # Delete our temp folder 54 | if ! [ $local ]; then 55 | cd .. 56 | rm -rf $DIR 57 | fi 58 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: RxDogTag 2 | repo_name: RxDogTag 3 | repo_url: https://github.com/uber/RxDogTag 4 | site_description: "Automatic tagging of RxJava 2 originating subscribe points for onError() investigation." 5 | site_author: Uber Technologies 6 | remote_branch: gh-pages 7 | 8 | copyright: 'Copyright © 2019 Uber Technologies' 9 | 10 | theme: 11 | name: 'material' 12 | favicon: images/uber_oss_logo.png 13 | logo: images/uber_oss_logo.png 14 | palette: 15 | primary: 'white' 16 | accent: 'white' 17 | 18 | extra_css: 19 | - 'css/app.css' 20 | 21 | markdown_extensions: 22 | - smarty 23 | - codehilite: 24 | guess_lang: false 25 | - footnotes 26 | - meta 27 | - toc: 28 | permalink: true 29 | - pymdownx.betterem: 30 | smart_enable: all 31 | - pymdownx.caret 32 | - pymdownx.inlinehilite 33 | - pymdownx.magiclink 34 | - pymdownx.smartsymbols 35 | - pymdownx.superfences 36 | - tables 37 | 38 | nav: 39 | - 'Overview': index.md 40 | - '1.X API': 41 | - 'rxdogtag': 1.x/rxdogtag/com.uber.rxdogtag/ 42 | - 'rxdogtag-autodispose': 1.x/rxdogtag-autodispose/com.uber.rxdogtag.autodispose/ 43 | - '2.X API': 44 | - 'rxdogtag': 2.x/rxdogtag/rxdogtag2/ 45 | - 'rxdogtag-autodispose': 2.x/rxdogtag-autodispose/rxdogtag2.autodispose2/ 46 | - 'Benchmark': benchmark.md 47 | - 'Examples': examples.md 48 | - 'Under The Hood': under-the-hood.md 49 | - 'Proguard/R8': shrinking.md 50 | - 'Changelog' : changelog.md 51 | - 'Contributing': contributing.md 52 | - 'Code of Conduct': code-of-conduct.md 53 | -------------------------------------------------------------------------------- /rxdogtag-autodispose/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'ru.vyarus.animalsniffer' 19 | id 'org.jetbrains.dokka' 20 | } 21 | 22 | dependencies { 23 | api project(':rxdogtag') 24 | 25 | implementation deps.rx.autodispose 26 | 27 | signature deps.build.animalSniffer 28 | testImplementation deps.test.junit 29 | testImplementation deps.test.truth 30 | testImplementation project(':rxdogtag').sourceSets.test.output 31 | } 32 | 33 | dokka { 34 | // Output as Github flavored markdown so that docs show up inline in mkdocs website 35 | outputFormat = 'gfm' 36 | outputDirectory = "$rootDir/docs/2.x" 37 | 38 | configuration { 39 | classpath = [project.tasks['assemble'].outputs.files.files] 40 | sourceRoot { 41 | path = "src/main/java" 42 | } 43 | externalDocumentationLink { 44 | url = new URL("http://reactivex.io/RxJava/3.x/javadoc/") 45 | } 46 | } 47 | } 48 | 49 | apply plugin: 'com.vanniktech.maven.publish' 50 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/EmptySubscriber.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import io.reactivex.rxjava3.core.FlowableSubscriber; 19 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 20 | import org.reactivestreams.Subscription; 21 | 22 | /** 23 | * Note we implement FlowableSubscriber here because it's required for CrashOnErrorObserver to work. 24 | * See its doc for more details. 25 | * 26 | * @param the type 27 | */ 28 | class EmptySubscriber implements FlowableSubscriber, LambdaConsumerIntrospection { 29 | 30 | @Override 31 | public void onSubscribe(Subscription s) { 32 | s.request(Long.MAX_VALUE); 33 | } 34 | 35 | @Override 36 | public void onNext(T t) {} 37 | 38 | @Override 39 | public final void onError(Throwable e) {} 40 | 41 | @Override 42 | public void onComplete() {} 43 | 44 | @Override 45 | public final boolean hasCustomOnError() { 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rxdogtag/src/main/java/rxdogtag2/RxDogTagTaggedExceptionReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 19 | 20 | /** 21 | * A marker type to indicate that RxDogTag's should pass the decorated stacktrace to {@link 22 | * #onError(Throwable)}. Note that this will always be tried directly, and no guarded delegate 23 | * behavior will be attempted even if {@link RxDogTag.Builder#guardObserverCallbacks(boolean)} is 24 | * enabled in configuration. 25 | * 26 | *

NOTE: RxDogTag exceptions are always {@link OnErrorNotImplementedException 27 | * OnErrorNotImplementedExceptions}, as these have special behavior as an escape hatch in RxJava 28 | * internals. This exception will have no "cause" property if the original exception was not already 29 | * an {@link OnErrorNotImplementedException}. If it was, which is unusual, it is reused with its 30 | * original cause (if any) and has its stacktrace modified. 31 | */ 32 | public interface RxDogTagTaggedExceptionReceiver extends RxDogTagErrorReceiver {} 33 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | pluginManagement { 18 | apply from: file('gradle/dependencies.gradle') 19 | resolutionStrategy { 20 | eachPlugin { 21 | switch (requested.id.id) { 22 | case 'me.champeau.gradle.jmh': 23 | useVersion(deps.versions.jmhPlugin) 24 | break 25 | case 'org.jetbrains.dokka': 26 | useModule("org.jetbrains.dokka:dokka-gradle-plugin:${deps.versions.dokka}") 27 | break 28 | case 'net.ltgt.errorprone': 29 | useVersion(deps.versions.errorPronePlugin) 30 | break 31 | case 'net.ltgt.nullaway': 32 | useVersion(deps.versions.nullawayPlugin) 33 | break 34 | case 'ru.vyarus.animalsniffer': 35 | useVersion(deps.versions.animalSniffer) 36 | break 37 | } 38 | } 39 | } 40 | repositories { 41 | mavenCentral() 42 | gradlePluginPortal() 43 | jcenter() 44 | } 45 | } 46 | 47 | rootProject.name = 'rxdogtag-root' 48 | if (System.getenv("ANDROID_HOME") != null) { 49 | include ':android-benchmark:benchmark' 50 | } 51 | include ':rxdogtag' 52 | include ':rxdogtag-autodispose' 53 | -------------------------------------------------------------------------------- /rxdogtag-autodispose/src/main/java/rxdogtag2/autodispose2/AutoDisposeConfigurer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2.autodispose2; 17 | 18 | import autodispose2.AutoDispose; 19 | import java.util.Collections; 20 | import java.util.Set; 21 | import rxdogtag2.ObserverHandler; 22 | import rxdogtag2.RxDogTag; 23 | 24 | /** 25 | * Configures an {@link RxDogTag.Builder} with: 26 | * 27 | *

    28 | *
  • A {@link ObserverHandler} that supports handling AutoDispose's decorating observers to 29 | * retrieve their underlying delegate observers. 30 | *
  • Ignored packages. 31 | *
32 | * 33 | *

Usage: Configure with {@link #configure(RxDogTag.Builder)}. 34 | */ 35 | public final class AutoDisposeConfigurer { 36 | 37 | private AutoDisposeConfigurer() {} 38 | 39 | private static final Set IGNORE_PACKAGES = 40 | Collections.singleton( 41 | // "com.uber.autodispose" 42 | AutoDispose.class.getPackage().getName()); 43 | 44 | public static void configure(RxDogTag.Builder builder) { 45 | builder 46 | .addObserverHandlers(AutoDisposeObserverHandler.INSTANCE) 47 | .addIgnoredPackages(IGNORE_PACKAGES); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rxdogtag/src/jmh/java/rxdogtag2/EventHandlingPerf.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import io.reactivex.rxjava3.core.Observer; 19 | import io.reactivex.rxjava3.observers.TestObserver; 20 | import org.openjdk.jmh.annotations.*; 21 | import org.openjdk.jmh.infra.Blackhole; 22 | import rxdogtag2.util.PerfObserver; 23 | 24 | /** 25 | * Measures the time that RxDogTag's event handling takes. 26 | * 27 | *

We only measure `Observer` here because the handling is currently identical across all 28 | * consumers. 29 | */ 30 | @BenchmarkMode(Mode.Throughput) 31 | @State(Scope.Thread) 32 | public class EventHandlingPerf { 33 | 34 | @Param({"false", "true"}) 35 | public boolean useRxDogTag; 36 | 37 | Observer observer = new TestObserver(); 38 | 39 | @Setup 40 | public void setup(final Blackhole bh) { 41 | Observer bhObserver = new PerfObserver(bh); 42 | 43 | if (useRxDogTag) { 44 | RxDogTag.Configuration configuration = new RxDogTag.Configuration(new RxDogTag.Builder()); 45 | observer = new DogTagObserver(configuration, bhObserver); 46 | } else { 47 | observer = bhObserver; 48 | } 49 | } 50 | 51 | @Benchmark 52 | public void onNext(Blackhole bh) { 53 | observer.onNext(12345); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###OSX### 2 | 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must ends with two \r. 8 | Icon 9 | 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear on external disk 15 | .Spotlight-V100 16 | .Trashes 17 | 18 | 19 | ###Linux### 20 | 21 | *~ 22 | 23 | # KDE directory preferences 24 | .directory 25 | 26 | 27 | ###Android### 28 | 29 | # Built application files 30 | *.apk 31 | *.ap_ 32 | 33 | # Files for ART and Dalvik VM 34 | *.dex 35 | 36 | # Java class files 37 | *.class 38 | 39 | # Generated files 40 | bin/ 41 | gen/ 42 | out/ 43 | 44 | # Gradle files 45 | .gradle/ 46 | .gradletasknamecache 47 | build/ 48 | 49 | # Local configuration file (sdk path, etc) 50 | local.properties 51 | 52 | # Lint 53 | lint-report.html 54 | lint-report_files/ 55 | lint_result.txt 56 | 57 | # Mobile Tools for Java (J2ME) 58 | .mtj.tmp/ 59 | 60 | # Package Files # 61 | *.war 62 | *.ear 63 | 64 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 65 | hs_err_pid* 66 | 67 | 68 | ###IntelliJ### 69 | 70 | *.iml 71 | *.ipr 72 | *.iws 73 | .idea/ 74 | 75 | 76 | ###Eclipse### 77 | 78 | *.pydevproject 79 | .metadata 80 | tmp/ 81 | *.tmp 82 | *.bak 83 | *.swp 84 | *~.nib 85 | .settings/ 86 | .loadpath 87 | 88 | # External tool builders 89 | .externalToolBuilders/ 90 | 91 | # Locally stored "Eclipse launch configurations" 92 | *.launch 93 | 94 | # CDT-specific 95 | .cproject 96 | 97 | # PDT-specific 98 | .buildpath 99 | 100 | # sbteclipse plugin 101 | .target 102 | 103 | # TeXlipse plugin 104 | .texlipse 105 | 106 | # kotlin 107 | annotations/ 108 | 109 | # Misc 110 | .vscode/ 111 | .project/ 112 | 113 | # Website 114 | docs/0.x/* 115 | docs/1.x/* 116 | docs/2.x/* 117 | docs/index.md 118 | docs/changelog.md 119 | docs/contributing.md 120 | docs/code-of-conduct.md 121 | -------------------------------------------------------------------------------- /gradle/dependencies.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | def versions = [ 18 | animalSniffer: '1.5.0', 19 | androidBenchmark: '1.0.0', 20 | errorProne: '2.3.3', 21 | errorPronePlugin: '0.7.1', 22 | gjf: '1.7', 23 | jmhLib: '1.21', 24 | jmhPlugin: '0.4.8', 25 | nullawayPlugin: '0.1', 26 | dokka: '0.10.1', 27 | spotless: '3.27.2' 28 | ] 29 | 30 | def build = [ 31 | ci: 'true' == System.getenv('CI'), 32 | javaVersion: JavaVersion.VERSION_1_8, 33 | 34 | errorProne: "com.google.errorprone:error_prone_core:${versions.errorProne}", 35 | errorProneJavac: "com.google.errorprone:javac:9+181-r4173-1", 36 | errorProneCheckApi: "com.google.errorprone:error_prone_check_api:${versions.errorProne}", 37 | errorProneTestHelpers: "com.google.errorprone:error_prone_test_helpers:${versions.errorProne}", 38 | nullAway: 'com.uber.nullaway:nullaway:0.7.9', 39 | animalSniffer: 'org.codehaus.mojo.signature:java17:1.0@signature' 40 | ] 41 | 42 | def rx = [ 43 | autodispose: 'com.uber.autodispose2:autodispose:2.0.0', 44 | java: 'io.reactivex.rxjava3:rxjava:3.0.0' 45 | ] 46 | 47 | def test = [ 48 | junit: 'junit:junit:4.12', 49 | truth: 'com.google.truth:truth:1.0.1' 50 | ] 51 | 52 | ext.deps = [ 53 | "build": build, 54 | "rx": rx, 55 | "test": test, 56 | "versions": versions 57 | ] 58 | -------------------------------------------------------------------------------- /rxdogtag/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'ru.vyarus.animalsniffer' 19 | id 'me.champeau.gradle.jmh' 20 | id 'org.jetbrains.dokka' 21 | } 22 | 23 | dependencies { 24 | api deps.rx.java 25 | 26 | signature deps.build.animalSniffer 27 | testImplementation deps.test.junit 28 | testImplementation deps.test.truth 29 | } 30 | 31 | dokka { 32 | // Output as Github flavored markdown so that docs show up inline in mkdocs website 33 | outputFormat = 'gfm' 34 | outputDirectory = "$rootDir/docs/2.x" 35 | 36 | configuration { 37 | platform = "JVM" 38 | classpath = [project.tasks['assemble'].outputs.files.files] 39 | sourceRoot { 40 | path = "src/main/java" 41 | } 42 | externalDocumentationLink { 43 | url = new URL("http://reactivex.io/RxJava/3.x/javadoc/") 44 | } 45 | } 46 | } 47 | 48 | // We setup jmh such that you must provide the `-Pjmh` to specify which tests 49 | // to run. If it has no value then all tests are run, but you can specify 50 | // tests by providing a value. (e.g., if you want to run "SubscriptionPerf", 51 | // you can run `./gradlew jmh -Pjmh=SubscriptionPerf`) 52 | jmh { 53 | jmhVersion = deps.versions.jmhLib 54 | 55 | if (project.hasProperty("jmh")) { 56 | include = [".*" + project.jmh + ".*"] 57 | println("JMH: " + include) 58 | } 59 | } 60 | 61 | apply plugin: 'com.vanniktech.maven.publish' 62 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to RxDogTag 2 | ======================== 3 | 4 | The Uber team welcomes contributions of all kinds, from simple bug reports through documentation, test cases, 5 | bugfixes, and features. 6 | 7 | Workflow 8 | -------- 9 | 10 | We love GitHub issues! 11 | 12 | For small feature requests, an issue first proposing it for discussion or demo implementation in a PR suffice. 13 | 14 | For big features, please open an issue so that we can agree on the direction, and hopefully avoid 15 | investing a lot of time on a feature that might need reworking. 16 | 17 | Small pull requests for things like typos, bugfixes, etc are always welcome. 18 | 19 | ### Code style 20 | 21 | This project uses [ktlint](https://github.com/pinterest/ktlint) and [GJF](https://github.com/google/google-java-format), 22 | provided via the [spotless](https://github.com/diffplug/spotless) gradle plugin. 23 | 24 | If you find that one of your pull reviews does not pass the CI server check due to a code style 25 | conflict, you can easily fix it by running: ./gradlew spotlessApply. 26 | 27 | Generally speaking - we use vanilla ktlint + 2space indents, and vanilla GJF. You can integrate both of 28 | these in IntelliJ code style via either [GJF's official plugin](https://plugins.jetbrains.com/plugin/8527-google-java-format) 29 | or applying code style from Jetbrains' official style. 30 | 31 | No star imports please! 32 | 33 | DOs and DON'Ts 34 | -------------- 35 | 36 | * DO follow our coding style 37 | * DO include tests when adding new features. When fixing bugs, start with adding a test that highlights how the current behavior is broken. 38 | * DO keep the discussions focused. When a new or related topic comes up it's often better to create new issue than to side track the discussion. 39 | * DO run all Gradle verification tasks (`./gradlew check`) before submitting a pull request 40 | 41 | * DON'T submit PRs that alter licensing related files or headers. If you believe there's a problem with them, file an issue and we'll be happy to discuss it. 42 | -------------------------------------------------------------------------------- /android-benchmark/benchmark/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 Uber Technologies, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: "com.android.library" 18 | apply plugin: "org.jetbrains.kotlin.android" 19 | apply plugin: "androidx.benchmark" 20 | 21 | android { 22 | compileSdkVersion 29 23 | 24 | defaultConfig { 25 | minSdkVersion 14 26 | targetSdkVersion 29 27 | } 28 | compileOptions { 29 | sourceCompatibility = JavaVersion.VERSION_1_8 30 | targetCompatibility = JavaVersion.VERSION_1_8 31 | } 32 | sourceSets { 33 | findByName("androidTest")?.java?.srcDirs("src/androidTest/kotlin") 34 | findByName("main")?.java?.srcDirs("src/main/kotlin") 35 | } 36 | } 37 | 38 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 39 | kotlinOptions { 40 | jvmTarget = "1.8" 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation "androidx.benchmark:benchmark-common:${deps.versions.androidBenchmark}" 46 | implementation "androidx.benchmark:benchmark-junit4:${deps.versions.androidBenchmark}" 47 | implementation "androidx.test:runner:1.2.0" 48 | implementation "androidx.test:rules:1.2.0" 49 | implementation "androidx.test.ext:junit:1.1.1" 50 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.50" 51 | implementation "io.reactivex.rxjava3:rxandroid:3.0.0" 52 | implementation deps.test.junit 53 | implementation project(":rxdogtag") 54 | androidTestImplementation configurations.getByName("implementation") 55 | } 56 | -------------------------------------------------------------------------------- /rxdogtag/src/jmh/java/rxdogtag2/SubscriptionPerf.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import io.reactivex.rxjava3.core.Observable; 19 | import io.reactivex.rxjava3.disposables.Disposable; 20 | import io.reactivex.rxjava3.functions.Consumer; 21 | import io.reactivex.rxjava3.internal.functions.Functions; 22 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 23 | import org.openjdk.jmh.annotations.*; 24 | 25 | /** Measures the time it takes to simply subscribe (but not consume any events). */ 26 | @BenchmarkMode(Mode.Throughput) 27 | @State(Scope.Thread) 28 | public class SubscriptionPerf { 29 | 30 | @Param({"false", "true"}) 31 | public boolean installRxDogTag; 32 | 33 | // Setup before test to narrow in on what we want to benchmark 34 | Observable emptyObservable = Observable.empty(); 35 | Consumer emptyStringConsumer = Functions.emptyConsumer(); 36 | Consumer emptyThrowableConsumer = Functions.emptyConsumer(); 37 | 38 | @Setup 39 | public void setup() { 40 | if (installRxDogTag) { 41 | RxDogTag.install(); 42 | } 43 | } 44 | 45 | @TearDown 46 | public void tearDown() { 47 | RxJavaPlugins.reset(); 48 | } 49 | 50 | @Benchmark 51 | public Disposable subscribeWithoutOnError() { 52 | return emptyObservable.subscribe(emptyStringConsumer); 53 | } 54 | 55 | @Benchmark 56 | public Disposable subscribeWithOnError() { 57 | return emptyObservable.subscribe(emptyStringConsumer, emptyThrowableConsumer); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/DogTagTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import static com.google.common.truth.Truth.assertThat; 19 | 20 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 21 | import java.util.Locale; 22 | import rxdogtag2.RxDogTag; 23 | 24 | interface DogTagTest { 25 | 26 | /** This basically tests {@link RxDogTag#guardedDelegateCall}. See its doc for more details. */ 27 | default void assertUnwrappedError( 28 | Throwable e, 29 | int expectedLineNumber, 30 | OnErrorNotImplementedException originalError, 31 | String delegateType) { 32 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 33 | assertThat(e).hasMessageThat().isEqualTo(originalError.getCause().getMessage()); 34 | assertThat(e.getStackTrace()).isEmpty(); 35 | Throwable cause = e.getCause(); 36 | assertThat(cause).isNotNull(); 37 | assertThat(cause.getStackTrace()[0].getFileName()) 38 | .isEqualTo(getClass().getSimpleName() + ".java"); 39 | assertThat(cause.getStackTrace()[0].getLineNumber()).isEqualTo(expectedLineNumber); 40 | assertThat(cause.getStackTrace()[1].getClassName()) 41 | .isEqualTo(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 42 | assertThat(cause.getStackTrace()[2].getClassName()) 43 | .isEqualTo(String.format(Locale.US, RxDogTag.STACK_ELEMENT_SOURCE_DELEGATE, delegateType)); 44 | assertThat(cause.getStackTrace()[3].getClassName()) 45 | .isEqualTo(RxDogTag.STACK_ELEMENT_TRACE_HEADER); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rxdogtag/src/jmh/java/rxdogtag2/EntireProcessPerf.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import io.reactivex.rxjava3.core.Flowable; 19 | import io.reactivex.rxjava3.core.Observable; 20 | import io.reactivex.rxjava3.disposables.Disposable; 21 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 22 | import java.util.Arrays; 23 | import org.openjdk.jmh.annotations.*; 24 | import org.openjdk.jmh.infra.Blackhole; 25 | import rxdogtag2.util.PerfConsumer; 26 | 27 | /** Measures the time it takes to both subscribe and consume events. */ 28 | @BenchmarkMode(Mode.Throughput) 29 | @State(Scope.Thread) 30 | public class EntireProcessPerf { 31 | 32 | @Param({"1", "1000", "1000000"}) 33 | public int times; 34 | 35 | @Param({"false", "true"}) 36 | public boolean installRxDogTag; 37 | 38 | Flowable flowable = Flowable.empty(); 39 | Observable observable = Observable.empty(); 40 | 41 | @Setup 42 | public void setup(Blackhole bh) { 43 | if (installRxDogTag) { 44 | RxDogTag.install(); 45 | } 46 | 47 | Integer[] array = new Integer[times]; 48 | Arrays.fill(array, 777); 49 | 50 | flowable = Flowable.fromArray(array); 51 | 52 | observable = Observable.fromArray(array); 53 | } 54 | 55 | @TearDown 56 | public void tearDown() { 57 | RxJavaPlugins.reset(); 58 | } 59 | 60 | @Benchmark 61 | public Disposable flowable(Blackhole bh) { 62 | return flowable.subscribe(new PerfConsumer(bh)); 63 | } 64 | 65 | @Benchmark 66 | public Disposable observable(Blackhole bh) { 67 | return observable.subscribe(new PerfConsumer(bh)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/ReadMeExample.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import io.reactivex.rxjava3.core.Observable; 19 | import io.reactivex.rxjava3.schedulers.Schedulers; 20 | import java.util.concurrent.CountDownLatch; 21 | import java.util.concurrent.TimeUnit; 22 | import org.junit.After; 23 | import org.junit.Ignore; 24 | import org.junit.Rule; 25 | import org.junit.Test; 26 | import rxdogtag2.RxDogTag; 27 | 28 | /** This is the code for the README examples. Un-ignore to test yourself. */ 29 | @Ignore 30 | @SuppressWarnings("CheckReturnValue") 31 | public class ReadMeExample { 32 | 33 | @Rule 34 | public AggressiveUncaughtExceptionHandlerRule handlerRule = 35 | new AggressiveUncaughtExceptionHandlerRule(); 36 | 37 | @After 38 | public void tearDown() { 39 | RxDogTag.reset(); 40 | } 41 | 42 | @Test 43 | public void simpleSubscribe() { 44 | RxDogTag.install(); 45 | Observable.error(new RuntimeException("Unhandled error!")).subscribe(); 46 | } 47 | 48 | @Test 49 | public void complex() throws InterruptedException { 50 | RxDogTag.install(); 51 | CountDownLatch latch = new CountDownLatch(1); 52 | Observable.just(1).subscribeOn(Schedulers.io()).map(i -> null).subscribe(); 53 | latch.await(1, TimeUnit.SECONDS); 54 | } 55 | 56 | @Test 57 | public void complexDelegate() throws InterruptedException { 58 | RxDogTag.install(); 59 | CountDownLatch latch = new CountDownLatch(1); 60 | Observable.just(1).subscribeOn(Schedulers.io()).subscribe(i -> throwSomething()); 61 | latch.await(1, TimeUnit.SECONDS); 62 | } 63 | 64 | private void throwSomething() { 65 | throw new RuntimeException("Unhandled error!"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/RxErrorsRule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import static com.google.common.truth.Truth.assertThat; 19 | 20 | import io.reactivex.rxjava3.exceptions.CompositeException; 21 | import io.reactivex.rxjava3.exceptions.UndeliverableException; 22 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | import java.util.NoSuchElementException; 26 | import java.util.concurrent.BlockingDeque; 27 | import java.util.concurrent.LinkedBlockingDeque; 28 | import java.util.concurrent.TimeUnit; 29 | import org.junit.rules.TestWatcher; 30 | import org.junit.runner.Description; 31 | 32 | /** JUnit rule to record RxJava errors. */ 33 | @SuppressWarnings("ThrowableResultOfMethodCallIgnored") 34 | public class RxErrorsRule extends TestWatcher { 35 | 36 | private BlockingDeque errors = new LinkedBlockingDeque<>(); 37 | 38 | @Override 39 | protected void starting(Description description) { 40 | RxJavaPlugins.setErrorHandler(t -> errors.add(t)); 41 | } 42 | 43 | @Override 44 | protected void finished(Description description) { 45 | RxJavaPlugins.setErrorHandler(null); 46 | } 47 | 48 | public List getErrors() { 49 | return new ArrayList<>(errors); 50 | } 51 | 52 | public Throwable take() { 53 | Throwable error; 54 | try { 55 | error = errors.pollFirst(1, TimeUnit.SECONDS); 56 | } catch (InterruptedException e) { 57 | throw new RuntimeException(e); 58 | } 59 | if (error == null) { 60 | throw new NoSuchElementException("No errors recorded."); 61 | } 62 | return error; 63 | } 64 | 65 | public Throwable takeThrowableFromUndeliverableException() { 66 | Throwable error = take(); 67 | assertThat(error).isInstanceOf(UndeliverableException.class); 68 | return error.getCause(); 69 | } 70 | 71 | public CompositeException takeCompositeException() { 72 | Throwable error = take(); 73 | assertThat(error).isInstanceOf(CompositeException.class); 74 | return (CompositeException) error; 75 | } 76 | 77 | public boolean hasErrors() { 78 | Throwable error = errors.peek(); 79 | return error != null; 80 | } 81 | 82 | public void assertNoErrors() { 83 | if (hasErrors()) { 84 | throw new AssertionError("Expected no errors but found " + getErrors()); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mobile-open-source@uber.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /rxdogtag-autodispose/src/main/java/rxdogtag2/autodispose2/AutoDisposeObserverHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2.autodispose2; 17 | 18 | import autodispose2.observers.AutoDisposingCompletableObserver; 19 | import autodispose2.observers.AutoDisposingMaybeObserver; 20 | import autodispose2.observers.AutoDisposingObserver; 21 | import autodispose2.observers.AutoDisposingSingleObserver; 22 | import autodispose2.observers.AutoDisposingSubscriber; 23 | import io.reactivex.rxjava3.core.Completable; 24 | import io.reactivex.rxjava3.core.CompletableObserver; 25 | import io.reactivex.rxjava3.core.Flowable; 26 | import io.reactivex.rxjava3.core.Maybe; 27 | import io.reactivex.rxjava3.core.MaybeObserver; 28 | import io.reactivex.rxjava3.core.Observable; 29 | import io.reactivex.rxjava3.core.Observer; 30 | import io.reactivex.rxjava3.core.Single; 31 | import io.reactivex.rxjava3.core.SingleObserver; 32 | import org.reactivestreams.Subscriber; 33 | import rxdogtag2.ObserverHandler; 34 | 35 | final class AutoDisposeObserverHandler implements ObserverHandler { 36 | 37 | static final AutoDisposeObserverHandler INSTANCE = new AutoDisposeObserverHandler(); 38 | 39 | private AutoDisposeObserverHandler() {} 40 | 41 | @Override 42 | public Subscriber handle(Flowable flowable, Subscriber subscriber) { 43 | if (subscriber instanceof AutoDisposingSubscriber) { 44 | return ((AutoDisposingSubscriber) subscriber).delegateSubscriber(); 45 | } 46 | return subscriber; 47 | } 48 | 49 | @Override 50 | public Observer handle(Observable observable, Observer observer) { 51 | if (observer instanceof AutoDisposingObserver) { 52 | return ((AutoDisposingObserver) observer).delegateObserver(); 53 | } 54 | return observer; 55 | } 56 | 57 | @Override 58 | public MaybeObserver handle(Maybe maybe, MaybeObserver observer) { 59 | if (observer instanceof AutoDisposingMaybeObserver) { 60 | return ((AutoDisposingMaybeObserver) observer).delegateObserver(); 61 | } 62 | return observer; 63 | } 64 | 65 | @Override 66 | public SingleObserver handle(Single single, SingleObserver observer) { 67 | if (observer instanceof AutoDisposingSingleObserver) { 68 | return ((AutoDisposingSingleObserver) observer).delegateObserver(); 69 | } 70 | return observer; 71 | } 72 | 73 | @Override 74 | public CompletableObserver handle(Completable completable, CompletableObserver observer) { 75 | if (observer instanceof AutoDisposingCompletableObserver) { 76 | return ((AutoDisposingCompletableObserver) observer).delegateObserver(); 77 | } 78 | return observer; 79 | } 80 | 81 | @Override 82 | public String toString() { 83 | return "AutoDisposeObserverHandler"; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/AggressiveUncaughtExceptionHandlerRule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import com.google.errorprone.annotations.concurrent.LazyInit; 19 | import java.io.PrintWriter; 20 | import java.io.StringWriter; 21 | import java.lang.Thread.UncaughtExceptionHandler; 22 | import java.util.Map; 23 | import java.util.concurrent.ConcurrentHashMap; 24 | import org.junit.rules.TestWatcher; 25 | import org.junit.runner.Description; 26 | import org.junit.runners.model.Statement; 27 | 28 | /** 29 | * A {@link TestWatcher} used to install an aggressive default {@link UncaughtExceptionHandler} 30 | * similar to the one found on Android. No exceptions should escape from RxDogTag that might cause 31 | * apps to be killed or tests to fail on Android. 32 | */ 33 | public final class AggressiveUncaughtExceptionHandlerRule extends TestWatcher { 34 | 35 | @LazyInit private UncaughtExceptionHandler oldDefaultUncaughtExceptionHandler; 36 | @LazyInit private Description lastTestStarted; 37 | private final Map exceptions = new ConcurrentHashMap<>(); 38 | 39 | @Override 40 | protected void starting(Description description) { 41 | System.err.println("Installing aggressive uncaught exception handler"); 42 | oldDefaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); 43 | Thread.setDefaultUncaughtExceptionHandler( 44 | (thread, throwable) -> { 45 | StringWriter errorText = new StringWriter(256); 46 | errorText.append("Uncaught exception in RxDogTag thread \""); 47 | errorText.append(thread.getName()); 48 | errorText.append("\"\n"); 49 | throwable.printStackTrace(new PrintWriter(errorText)); 50 | errorText.append("\n"); 51 | if (lastTestStarted != null) { 52 | errorText.append("Last test to start was: "); 53 | errorText.append(lastTestStarted.getDisplayName()); 54 | errorText.append("\n"); 55 | } 56 | System.err.print(errorText.toString()); 57 | 58 | exceptions.put(throwable, lastTestStarted.getDisplayName()); 59 | }); 60 | } 61 | 62 | @Override 63 | public Statement apply(Statement base, Description description) { 64 | lastTestStarted = description; 65 | return super.apply(base, description); 66 | } 67 | 68 | @Override 69 | protected void finished(Description description) { 70 | Thread.setDefaultUncaughtExceptionHandler(oldDefaultUncaughtExceptionHandler); 71 | System.err.println("Uninstalled aggressive uncaught exception handler"); 72 | 73 | if (!exceptions.isEmpty()) { 74 | sneakyThrow(exceptions.keySet().iterator().next()); 75 | } 76 | } 77 | 78 | private static void sneakyThrow(Throwable t) throws T { 79 | //noinspection unchecked 80 | throw (T) t; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /rxdogtag/src/main/java/rxdogtag2/DogTagCompletableObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import static rxdogtag2.RxDogTag.createException; 19 | import static rxdogtag2.RxDogTag.guardedDelegateCall; 20 | import static rxdogtag2.RxDogTag.reportError; 21 | 22 | import io.reactivex.rxjava3.core.CompletableObserver; 23 | import io.reactivex.rxjava3.disposables.Disposable; 24 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 25 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 26 | 27 | /** 28 | * A delegating {@link CompletableObserver} that throws {@link OnErrorNotImplementedException} with 29 | * stack element tagging to indicate the exact line number that was subscribed. 30 | * 31 | *

NOTE: This will also capture exceptions thrown by the {@link #delegate "delegate"} 's 32 | * {@link CompletableObserver#onSubscribe(Disposable) onSubscribe()} or {@link 33 | * CompletableObserver#onComplete()} onComplete()} methods and route them through the more specific 34 | * {@link #onError(Throwable) onError()} in this class for better tagging. 35 | */ 36 | final class DogTagCompletableObserver implements CompletableObserver, LambdaConsumerIntrospection { 37 | 38 | private final Throwable t = new Throwable(); 39 | private final RxDogTag.Configuration config; 40 | private final CompletableObserver delegate; 41 | 42 | DogTagCompletableObserver(RxDogTag.Configuration config, CompletableObserver delegate) { 43 | this.config = config; 44 | this.delegate = delegate; 45 | } 46 | 47 | @Override 48 | public void onSubscribe(Disposable d) { 49 | if (config.guardObserverCallbacks) { 50 | guardedDelegateCall( 51 | e -> reportError(config, t, e, "onSubscribe"), () -> delegate.onSubscribe(d)); 52 | } else { 53 | delegate.onSubscribe(d); 54 | } 55 | } 56 | 57 | @Override 58 | public void onError(Throwable e) { 59 | if (delegate instanceof RxDogTagErrorReceiver) { 60 | if (delegate instanceof RxDogTagTaggedExceptionReceiver) { 61 | delegate.onError(createException(config, t, e, null)); 62 | } else if (config.guardObserverCallbacks) { 63 | guardedDelegateCall(e2 -> reportError(config, t, e2, "onError"), () -> delegate.onError(e)); 64 | } else { 65 | delegate.onError(e); 66 | } 67 | } else { 68 | reportError(config, t, e, null); 69 | } 70 | } 71 | 72 | @Override 73 | public void onComplete() { 74 | if (config.guardObserverCallbacks) { 75 | guardedDelegateCall(e -> reportError(config, t, e, "onComplete"), delegate::onComplete); 76 | } else { 77 | delegate.onComplete(); 78 | } 79 | } 80 | 81 | @Override 82 | public boolean hasCustomOnError() { 83 | return delegate instanceof LambdaConsumerIntrospection 84 | && ((LambdaConsumerIntrospection) delegate).hasCustomOnError(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /rxdogtag/src/main/java/rxdogtag2/DogTagSingleObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import static rxdogtag2.RxDogTag.createException; 19 | import static rxdogtag2.RxDogTag.guardedDelegateCall; 20 | import static rxdogtag2.RxDogTag.reportError; 21 | 22 | import io.reactivex.rxjava3.core.SingleObserver; 23 | import io.reactivex.rxjava3.disposables.Disposable; 24 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 25 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 26 | 27 | /** 28 | * A delegating {@link SingleObserver} that throws {@link OnErrorNotImplementedException} with stack 29 | * element tagging to indicate the exact line number that was subscribed. 30 | * 31 | *

NOTE: This will also capture exceptions thrown by the {@link #delegate "delegate"} 's 32 | * {@link SingleObserver#onSubscribe(Disposable) onSubscribe()} or {@link 33 | * SingleObserver#onSuccess(Object)} onNext()} methods and route them through the more specific 34 | * {@link #onError(Throwable) onError()} in this class for better tagging. 35 | * 36 | * @param The type 37 | */ 38 | final class DogTagSingleObserver implements SingleObserver, LambdaConsumerIntrospection { 39 | 40 | private final Throwable t = new Throwable(); 41 | private final RxDogTag.Configuration config; 42 | private final SingleObserver delegate; 43 | 44 | DogTagSingleObserver(RxDogTag.Configuration config, SingleObserver delegate) { 45 | this.config = config; 46 | this.delegate = delegate; 47 | } 48 | 49 | @Override 50 | public void onSubscribe(Disposable d) { 51 | if (config.guardObserverCallbacks) { 52 | guardedDelegateCall( 53 | e -> reportError(config, t, e, "onSubscribe"), () -> delegate.onSubscribe(d)); 54 | } else { 55 | delegate.onSubscribe(d); 56 | } 57 | } 58 | 59 | @Override 60 | public void onSuccess(T t) { 61 | if (config.guardObserverCallbacks) { 62 | guardedDelegateCall( 63 | e -> reportError(config, this.t, e, "onSuccess"), () -> delegate.onSuccess(t)); 64 | } else { 65 | delegate.onSuccess(t); 66 | } 67 | } 68 | 69 | @Override 70 | public void onError(Throwable e) { 71 | if (delegate instanceof RxDogTagErrorReceiver) { 72 | if (delegate instanceof RxDogTagTaggedExceptionReceiver) { 73 | delegate.onError(createException(config, t, e, null)); 74 | } else if (config.guardObserverCallbacks) { 75 | guardedDelegateCall(e2 -> reportError(config, t, e2, "onError"), () -> delegate.onError(e)); 76 | } else { 77 | delegate.onError(e); 78 | } 79 | } else { 80 | reportError(config, t, e, null); 81 | } 82 | } 83 | 84 | @Override 85 | public boolean hasCustomOnError() { 86 | return delegate instanceof LambdaConsumerIntrospection 87 | && ((LambdaConsumerIntrospection) delegate).hasCustomOnError(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/ObserverHandlerDefaultsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import static com.google.common.truth.Truth.assertThat; 19 | 20 | import io.reactivex.rxjava3.core.Completable; 21 | import io.reactivex.rxjava3.core.Flowable; 22 | import io.reactivex.rxjava3.core.Maybe; 23 | import io.reactivex.rxjava3.core.Observable; 24 | import io.reactivex.rxjava3.core.Single; 25 | import io.reactivex.rxjava3.observers.TestObserver; 26 | import io.reactivex.rxjava3.subscribers.TestSubscriber; 27 | import org.junit.Test; 28 | import rxdogtag2.ObserverHandler; 29 | import rxdogtag2.RxDogTag; 30 | 31 | public final class ObserverHandlerDefaultsTest { 32 | 33 | private ObserverHandler handler = new ObserverHandler() {}; 34 | 35 | @Test 36 | public void defaultsShouldReturnSameObserver() { 37 | // Using TestObserver out of convenience since it implements every Observer type. 38 | TestObserver o = new TestObserver<>(); 39 | TestSubscriber s = new TestSubscriber<>(); 40 | 41 | assertThat(handler.handle(Flowable.never(), s)).isSameInstanceAs(s); 42 | assertThat(handler.handle(Observable.never(), o)).isSameInstanceAs(o); 43 | assertThat(handler.handle(Maybe.never(), o)).isSameInstanceAs(o); 44 | assertThat(handler.handle(Single.never(), o)).isSameInstanceAs(o); 45 | assertThat(handler.handle(Completable.never(), o)).isSameInstanceAs(o); 46 | } 47 | 48 | /** 49 | * This is a weird but necessary tests. We have proguard configurations that depend on these 50 | * packages to have names kept in order for DogTagObservers to work their magic correctly. 51 | * 52 | *

In the event that this test fails, please update the proguard configurations with the new 53 | * package names. You will see something like this the bundled proguard config. 54 | * 55 | *


56 |    *   -keeppackagenames io.reactivex**
57 |    * 
58 | * 59 | *

This should be updated with the new package name. 60 | */ 61 | @Test 62 | public void verifyPackages() { 63 | checkPackage("Observable", "io.reactivex.rxjava3", "io.reactivex.rxjava3"); 64 | checkPackage("RxDogTag", "rxdogtag2", RxDogTag.class.getPackage().getName()); 65 | } 66 | 67 | private void checkPackage(String name, String expectedPackage, String actualPackage) { 68 | if (!actualPackage.equals(expectedPackage)) { 69 | throw new RuntimeException( 70 | "The package name for " 71 | + name 72 | + " has changed. Please " 73 | + "update this test and the proguard -keepnames configuration to properly keep the new " 74 | + "package It was \n\"-keepnames class " 75 | + expectedPackage 76 | + ".**\"\nbut should now " 77 | + "be\n\"-keepnames class " 78 | + actualPackage 79 | + ".**\""); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /rxdogtag/src/main/java/rxdogtag2/DogTagObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import static rxdogtag2.RxDogTag.createException; 19 | import static rxdogtag2.RxDogTag.guardedDelegateCall; 20 | import static rxdogtag2.RxDogTag.reportError; 21 | 22 | import io.reactivex.rxjava3.core.Observer; 23 | import io.reactivex.rxjava3.disposables.Disposable; 24 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 25 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 26 | 27 | /** 28 | * A delegating {@link Observer} that throws {@link OnErrorNotImplementedException} with stack 29 | * element tagging to indicate the exact line number that was subscribed. 30 | * 31 | *

NOTE: This will also capture exceptions thrown by the {@link #delegate "delegate"} 's 32 | * {@link Observer#onSubscribe(Disposable) onSubscribe()}, {@link Observer#onNext(Object)} 33 | * onNext()}, or {@link Observer#onComplete()} onComplete()} methods and route them through the more 34 | * specific {@link #onError(Throwable) onError()} in this class for better tagging. 35 | * 36 | * @param The type 37 | */ 38 | final class DogTagObserver implements Observer, LambdaConsumerIntrospection { 39 | 40 | private final Throwable t = new Throwable(); 41 | private final RxDogTag.Configuration config; 42 | private final Observer delegate; 43 | 44 | DogTagObserver(RxDogTag.Configuration config, Observer delegate) { 45 | this.config = config; 46 | this.delegate = delegate; 47 | } 48 | 49 | @Override 50 | public void onSubscribe(Disposable d) { 51 | if (config.guardObserverCallbacks) { 52 | guardedDelegateCall( 53 | e -> reportError(config, t, e, "onSubscribe"), () -> delegate.onSubscribe(d)); 54 | } else { 55 | delegate.onSubscribe(d); 56 | } 57 | } 58 | 59 | @Override 60 | public void onNext(T t) { 61 | if (config.guardObserverCallbacks) { 62 | guardedDelegateCall(e -> reportError(config, this.t, e, "onNext"), () -> delegate.onNext(t)); 63 | } else { 64 | delegate.onNext(t); 65 | } 66 | } 67 | 68 | @Override 69 | public void onError(Throwable e) { 70 | if (delegate instanceof RxDogTagErrorReceiver) { 71 | if (delegate instanceof RxDogTagTaggedExceptionReceiver) { 72 | delegate.onError(createException(config, t, e, null)); 73 | } else if (config.guardObserverCallbacks) { 74 | guardedDelegateCall(e2 -> reportError(config, t, e2, "onError"), () -> delegate.onError(e)); 75 | } else { 76 | delegate.onError(e); 77 | } 78 | } else { 79 | reportError(config, t, e, null); 80 | } 81 | } 82 | 83 | @Override 84 | public void onComplete() { 85 | if (config.guardObserverCallbacks) { 86 | guardedDelegateCall(e -> reportError(config, t, e, "onComplete"), delegate::onComplete); 87 | } else { 88 | delegate.onComplete(); 89 | } 90 | } 91 | 92 | @Override 93 | public boolean hasCustomOnError() { 94 | return delegate instanceof LambdaConsumerIntrospection 95 | && ((LambdaConsumerIntrospection) delegate).hasCustomOnError(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /rxdogtag/src/main/java/rxdogtag2/ObserverHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import io.reactivex.rxjava3.core.Completable; 19 | import io.reactivex.rxjava3.core.CompletableObserver; 20 | import io.reactivex.rxjava3.core.Flowable; 21 | import io.reactivex.rxjava3.core.Maybe; 22 | import io.reactivex.rxjava3.core.MaybeObserver; 23 | import io.reactivex.rxjava3.core.Observable; 24 | import io.reactivex.rxjava3.core.Observer; 25 | import io.reactivex.rxjava3.core.Single; 26 | import io.reactivex.rxjava3.core.SingleObserver; 27 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 28 | import org.reactivestreams.Subscriber; 29 | 30 | /** 31 | * This interface can be used to further decorate or unpack custom subscribers/observers as needed. 32 | * 33 | *

Example use cases: 34 | * 35 | *

    36 | *
  • Unpacking custom observers to yield their underlying delegate observers or {@link 37 | * LambdaConsumerIntrospection} behavior. 38 | *
  • Decorating observers with custom runtime behavior. 39 | *
40 | * 41 | *

Note: The observer returned here will NOT be used for anything other than determining 42 | * if there is custom error handling. 43 | */ 44 | public interface ObserverHandler { 45 | 46 | /** 47 | * Callbacks to handle {@link Flowable} and {@link Subscriber}. 48 | * 49 | * @param flowable the source flowable. 50 | * @param subscriber the source subscriber. 51 | * @return possible decorated or unpacked observer. 52 | */ 53 | default Subscriber handle(Flowable flowable, Subscriber subscriber) { 54 | return subscriber; 55 | } 56 | 57 | /** 58 | * Callbacks to handle {@link Observable} and {@link Observer}. 59 | * 60 | * @param observable the source observable. 61 | * @param observer the source observer. 62 | * @return possible decorated or unpacked observer. 63 | */ 64 | default Observer handle(Observable observable, Observer observer) { 65 | return observer; 66 | } 67 | 68 | /** 69 | * Callbacks to handle {@link Maybe} and {@link MaybeObserver}. 70 | * 71 | * @param maybe the source maybe. 72 | * @param observer the source observer. 73 | * @return possible decorated or unpacked observer. 74 | */ 75 | default MaybeObserver handle(Maybe maybe, MaybeObserver observer) { 76 | return observer; 77 | } 78 | 79 | /** 80 | * Callbacks to handle {@link Single} and {@link SingleObserver}. 81 | * 82 | * @param single the source single. 83 | * @param observer the source observer. 84 | * @return possible decorated or unpacked observer. 85 | */ 86 | default SingleObserver handle(Single single, SingleObserver observer) { 87 | return observer; 88 | } 89 | 90 | /** 91 | * Callbacks to handle {@link Completable} and {@link CompletableObserver}. 92 | * 93 | * @param completable the source completable. 94 | * @param observer the source observer. 95 | * @return possible decorated or unpacked observer. 96 | */ 97 | default CompletableObserver handle(Completable completable, CompletableObserver observer) { 98 | return observer; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /rxdogtag/src/main/java/rxdogtag2/DogTagMaybeObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import static rxdogtag2.RxDogTag.createException; 19 | import static rxdogtag2.RxDogTag.guardedDelegateCall; 20 | import static rxdogtag2.RxDogTag.reportError; 21 | 22 | import io.reactivex.rxjava3.core.MaybeObserver; 23 | import io.reactivex.rxjava3.disposables.Disposable; 24 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 25 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 26 | 27 | /** 28 | * A delegating {@link MaybeObserver} that throws {@link OnErrorNotImplementedException} with stack 29 | * element tagging to indicate the exact line number that was subscribed. 30 | * 31 | *

NOTE: This will also capture exceptions thrown by the {@link #delegate "delegate"} 's 32 | * {@link MaybeObserver#onSubscribe(Disposable) onSubscribe()}, {@link 33 | * MaybeObserver#onSuccess(Object)} onNext()}, or {@link MaybeObserver#onComplete()} onComplete()} 34 | * methods and route them through the more specific {@link #onError(Throwable) onError()} in this 35 | * class for better tagging. 36 | * 37 | * @param The type 38 | */ 39 | final class DogTagMaybeObserver implements MaybeObserver, LambdaConsumerIntrospection { 40 | 41 | private final Throwable t = new Throwable(); 42 | private final RxDogTag.Configuration config; 43 | private final MaybeObserver delegate; 44 | 45 | DogTagMaybeObserver(RxDogTag.Configuration config, MaybeObserver delegate) { 46 | this.config = config; 47 | this.delegate = delegate; 48 | } 49 | 50 | @Override 51 | public void onSubscribe(Disposable d) { 52 | if (config.guardObserverCallbacks) { 53 | guardedDelegateCall( 54 | e -> reportError(config, t, e, "onSubscribe"), () -> delegate.onSubscribe(d)); 55 | } else { 56 | delegate.onSubscribe(d); 57 | } 58 | } 59 | 60 | @Override 61 | public void onSuccess(T t) { 62 | if (config.guardObserverCallbacks) { 63 | guardedDelegateCall( 64 | e -> reportError(config, this.t, e, "onSuccess"), () -> delegate.onSuccess(t)); 65 | } else { 66 | delegate.onSuccess(t); 67 | } 68 | } 69 | 70 | @Override 71 | public void onError(Throwable e) { 72 | if (delegate instanceof RxDogTagErrorReceiver) { 73 | if (delegate instanceof RxDogTagTaggedExceptionReceiver) { 74 | delegate.onError(createException(config, t, e, null)); 75 | } else if (config.guardObserverCallbacks) { 76 | guardedDelegateCall(e2 -> reportError(config, t, e2, "onError"), () -> delegate.onError(e)); 77 | } else { 78 | delegate.onError(e); 79 | } 80 | } else { 81 | reportError(config, t, e, null); 82 | } 83 | } 84 | 85 | @Override 86 | public void onComplete() { 87 | if (config.guardObserverCallbacks) { 88 | guardedDelegateCall(e -> reportError(config, t, e, "onComplete"), delegate::onComplete); 89 | } else { 90 | delegate.onComplete(); 91 | } 92 | } 93 | 94 | @Override 95 | public boolean hasCustomOnError() { 96 | return delegate instanceof LambdaConsumerIntrospection 97 | && ((LambdaConsumerIntrospection) delegate).hasCustomOnError(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /rxdogtag/src/main/java/rxdogtag2/DogTagSubscriber.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rxdogtag2; 17 | 18 | import static rxdogtag2.RxDogTag.createException; 19 | import static rxdogtag2.RxDogTag.guardedDelegateCall; 20 | import static rxdogtag2.RxDogTag.reportError; 21 | 22 | import io.reactivex.rxjava3.core.FlowableSubscriber; 23 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 24 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 25 | import org.reactivestreams.Subscriber; 26 | import org.reactivestreams.Subscription; 27 | 28 | /** 29 | * A delegating {@link Subscriber} that throws {@link OnErrorNotImplementedException} with stack 30 | * element tagging to indicate the exact line number that was subscribed. 31 | * 32 | *

NOTE: This will also capture exceptions thrown by the {@link #delegate "delegate"} 's 33 | * {@link Subscriber#onSubscribe(Subscription) onSubscribe()}, {@link Subscriber#onNext(Object)} 34 | * onNext()}, or {@link Subscriber#onComplete()} onComplete()} methods and route them through the 35 | * more specific {@link #onError(Throwable) onError()} in this class for better tagging. 36 | * 37 | *

Due to restrictions in the ReactiveStreams spec, delegate exception capturing will only work 38 | * on the more lenient {@link FlowableSubscriber}, which allows callback exceptions to be rerouted 39 | * through {@link #onError(Throwable) onError()}. This is also why this class implements that 40 | * interface. 41 | * 42 | * @param The type 43 | */ 44 | final class DogTagSubscriber implements FlowableSubscriber, LambdaConsumerIntrospection { 45 | 46 | private final Throwable t = new Throwable(); 47 | private final RxDogTag.Configuration config; 48 | private final Subscriber delegate; 49 | 50 | DogTagSubscriber(RxDogTag.Configuration config, Subscriber delegate) { 51 | this.config = config; 52 | this.delegate = delegate; 53 | } 54 | 55 | @Override 56 | public void onSubscribe(Subscription s) { 57 | if (config.guardObserverCallbacks) { 58 | guardedDelegateCall( 59 | e -> reportError(config, t, e, "onSubscribe"), () -> delegate.onSubscribe(s)); 60 | } else { 61 | delegate.onSubscribe(s); 62 | } 63 | } 64 | 65 | @Override 66 | public void onNext(T t) { 67 | if (config.guardObserverCallbacks) { 68 | guardedDelegateCall(e -> reportError(config, this.t, e, "onNext"), () -> delegate.onNext(t)); 69 | } else { 70 | delegate.onNext(t); 71 | } 72 | } 73 | 74 | @Override 75 | public void onError(Throwable e) { 76 | if (delegate instanceof RxDogTagErrorReceiver) { 77 | if (delegate instanceof RxDogTagTaggedExceptionReceiver) { 78 | delegate.onError(createException(config, t, e, null)); 79 | } else if (config.guardObserverCallbacks) { 80 | guardedDelegateCall(e2 -> reportError(config, t, e2, "onError"), () -> delegate.onError(e)); 81 | } else { 82 | delegate.onError(e); 83 | } 84 | } else { 85 | reportError(config, t, e, null); 86 | } 87 | } 88 | 89 | @Override 90 | public void onComplete() { 91 | if (config.guardObserverCallbacks) { 92 | guardedDelegateCall(e -> reportError(config, t, e, "onComplete"), delegate::onComplete); 93 | } else { 94 | delegate.onComplete(); 95 | } 96 | } 97 | 98 | @Override 99 | public boolean hasCustomOnError() { 100 | return delegate instanceof LambdaConsumerIntrospection 101 | && ((LambdaConsumerIntrospection) delegate).hasCustomOnError(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /docs/benchmark.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | ## Running the benchmark 4 | 5 | You can run the benchmark by executing `./gradlew :android-benchmark:benchmark:connectedCheck`. Note that the benchmark should be run on an actual device 6 | 7 | You can then take the output from the benchmark and run it through `DataParser.kt` to get a structured breakdown as seen below. 8 | 9 | It's important to look at the units. In general, RxDogTag does add some overhead to your RxJava subscriptions but that overhead is irrelevant in the larger execution context of the code around it (less than a millisecond in most cases). 10 | 11 | Running the benchmark on a [Pixel 3](https://store.google.com/product/pixel_3_specs) yields the below results. 12 | 13 | ## Event throughput: grouped by number of events 14 | 15 | _Measures the amount of time it takes for given number of elements to pass through the stream._ 16 | 17 | ### Simple: 1 item (Observable) 18 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 19 | |----------|----------|------------|-----------| 20 | | false | false | 0.001ms | 532ns | 21 | | true | false | 0.007ms | 7,060ns | 22 | | true | true | 0.007ms | 7,378ns | 23 | 24 | ### Complex: 1 item (Observable) 25 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 26 | |----------|----------|------------|-----------| 27 | | true | false | 0.049ms | 49,195ns | 28 | | true | true | 0.050ms | 50,492ns | 29 | | false | false | 0.054ms | 53,672ns | 30 | 31 | ### Simple: 1_000 items (Observable) 32 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 33 | |----------|----------|------------|-----------| 34 | | false | false | 0.024ms | 23,968ns | 35 | | true | false | 0.041ms | 40,743ns | 36 | | true | true | 0.153ms | 152,943ns | 37 | 38 | ### Complex: 1_000 items (Observable) 39 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 40 | |----------|----------|------------|-----------| 41 | | false | false | 0.291ms | 291,301ns | 42 | | true | false | 0.313ms | 312,864ns | 43 | | true | true | 0.313ms | 313,334ns | 44 | 45 | ### Simple: 1_000_000 items (Observable) 46 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 47 | |----------|----------|------------|-----------| 48 | | false | false | 23.994ms | 23,993,700ns | 49 | | true | false | 27.305ms | 27,304,847ns | 50 | | true | true | 166.887ms | 166,887,047ns | 51 | 52 | ### Complex: 1_000_000 items (Observable) 53 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 54 | |----------|----------|------------|-----------| 55 | | true | false | 249.740ms | 249,739,764ns | 56 | | false | false | 252.728ms | 252,727,577ns | 57 | | true | true | 257.554ms | 257,553,671ns | 58 | 59 | ### Simple: 1 item (Flowable) 60 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 61 | |----------|----------|------------|-----------| 62 | | false | false | 0.001ms | 519ns | 63 | | true | false | 0.007ms | 7,234ns | 64 | | true | true | 0.008ms | 7,581ns | 65 | 66 | ### Complex: 1 item (Flowable) 67 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 68 | |----------|----------|------------|-----------| 69 | | true | false | 0.050ms | 50,081ns | 70 | | true | true | 0.051ms | 50,787ns | 71 | | false | false | 0.056ms | 56,004ns | 72 | 73 | ### Simple: 1_000 items (Flowable) 74 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 75 | |----------|----------|------------|-----------| 76 | | false | false | 0.025ms | 24,920ns | 77 | | true | false | 0.041ms | 40,568ns | 78 | | true | true | 0.153ms | 153,124ns | 79 | 80 | ### Complex: 1_000 items (Flowable) 81 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 82 | |----------|----------|------------|-----------| 83 | | false | false | 0.273ms | 273,178ns | 84 | | true | true | 0.343ms | 342,812ns | 85 | | true | false | 0.375ms | 375,208ns | 86 | 87 | ### Simple: 1_000_000 items (Flowable) 88 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 89 | |----------|----------|------------|-----------| 90 | | false | false | 23.953ms | 23,952,919ns | 91 | | true | false | 26.792ms | 26,791,825ns | 92 | | true | true | 162.547ms | 162,547,359ns | 93 | 94 | ### Complex: 1_000_000 items (Flowable) 95 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 96 | |----------|----------|------------|-----------| 97 | | true | true | 300.186ms | 300,186,228ns | 98 | | true | false | 302.881ms | 302,880,498ns | 99 | | false | false | 304.952ms | 304,952,062ns | 100 | 101 | 102 | ## Subscribe cost: grouped by complexity 103 | 104 | _Measures the cost to subscription incurred by RxDogTag. Subscription means no emissions, subscription only._ 105 | 106 | ### Simple (Observable) 107 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 108 | |----------|----------|------------|-----------| 109 | | false | false | 0.000ms | 331ns | 110 | | true | false | 0.007ms | 7,275ns | 111 | 112 | ### Simple (Flowable) 113 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 114 | |----------|----------|------------|-----------| 115 | | false | false | 0.000ms | 365ns | 116 | | true | false | 0.008ms | 7,506ns | 117 | 118 | ### Complex (Observable) 119 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 120 | |----------|----------|------------|-----------| 121 | | false | false | 0.002ms | 1,673ns | 122 | | true | false | 0.010ms | 9,770ns | 123 | 124 | ### Complex (Flowable) 125 | | RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) | 126 | |----------|----------|------------|-----------| 127 | | false | false | 0.006ms | 5,741ns | 128 | | true | false | 0.021ms | 20,783ns | 129 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 2.0.2 5 | ------------- 6 | 7 | _2023-03-09_ 8 | 9 | Fix embedded proguard rules to use `-keepnames` instead of `-keeppackagenames` as the latter isn't respected by R8 3+ when `repackageclasses` is enabled. 10 | 11 | Thanks to [@viakunin](https://github.com/viakunin) for contributing this fix! 12 | 13 | Version 1.0.2 14 | ------------- 15 | 16 | _2023-03-09_ 17 | 18 | Same proguard changes as 2.0.2 but for RxJava 2.x/RxDogTag 1.x. 19 | 20 | Version 2.0.1 21 | ------------- 22 | 23 | _2020-10-26_ 24 | 25 | Fix embedded proguard rules to completely keep `subscribe()` methods and rxdogtag code. R8 may 26 | inline `subscribe()` calls entirely to their call-sites, which breaks RxDogTag's tagging. 27 | 28 | While this blanket keep is sort of a cardinal sin of libraries to do this, RxDogTag is only a few 29 | classes and incredibly small, so we claim this is a small price to pay for actionable stacktraces. 30 | 31 | Version 1.0.1 32 | ------------- 33 | 34 | _2020-10-26_ 35 | 36 | Same proguard changes as 2.0.1 but for RxJava 2.x/RxDogTag 1.x. 37 | 38 | Version 2.0.0 39 | ------------- 40 | 41 | _2020-03-08_ 42 | 43 | RxDogTag 2 is built against RxJava 3 and is binary-compatible with RxDogTag 1.x and RxJava 2.x. As such - it has a different package name and maven group ID. 44 | 45 | ### Packaging 46 | 47 | | | Maven Group ID | Package Name | 48 | | --- | --- | --- | 49 | | 1.x | `com.uber.rxdogtag` | `com.uber.rxdogtag` | 50 | | 2.x | `com.uber.rxdogtag2` | `rxdogtag2` | 51 | 52 | For any sub-packages, the above mapping should be used for those package prefix replacements as well. 53 | 54 | ### Changes 55 | 56 | * Dependency Upgrades 57 | ``` 58 | RxJava 3.0.0 59 | AutoDispose 2.0.0 60 | ``` 61 | 62 | Version 1.0.0 63 | ------------- 64 | 65 | _2020-03-07_ 66 | 67 | * Stable release! 68 | * This release is identical in functionality to 0.3.0. 69 | * Various dependency upgrades. ([#55](https://github.com/uber/RxDogTag/pull/55)) 70 | ``` 71 | RxJava 2.2.18 72 | AutoDispose 1.4.0 73 | ``` 74 | 75 | Version 0.3.0 76 | ------------- 77 | 78 | _2019-09-23_ 79 | 80 | **New: Builder option to disable guarded observer callbacks. [#43](https://github.com/uber/RxDogTag/pull/43)** 81 | 82 | By default, RxDogTag will try/catch every onNext/onSuccess/onComplete calls in its observers to try to catch exceptions 83 | and modify the trace before they're handed to the (unhandled) onError. If you only want to handle 84 | upstream errors, you can disable this behavior in the builder now. This can be useful in hot paths 85 | where performance is more of a concern. 86 | 87 | ```java 88 | RxDogTag.builder() 89 | .guardObserverCallbacks(false) 90 | .install(); 91 | ``` 92 | 93 | **New: ErrorReceivers API. [#45](https://github.com/uber/RxDogTag/pull/45)** 94 | 95 | For users that have custom onError() implementations but want to supplement it with RxDogTag, there are 96 | two new APIs to help with this. 97 | 98 | `RxDogTagErrorReceiver` is a simple interface that your custom observer can implement to indicate that 99 | it wants its onError() method to be called with the aforementioned guarded observer callback. Note 100 | however that modified tagged exceptions in this case will be delivered to `RxJavaPlugins.onError()`. 101 | 102 | `RxDogTagTaggedExceptionReceiver` is an interface that your custom observer can implement to indicate 103 | that it wants its onError() called with the modified tagged exception. This is useful if you have 104 | custom onError() handling but want to use/report RxDogTag's tagged information for something custom. 105 | 106 | **Misc** 107 | - Proguard file has been tuned to use the more idiomatic `-keeppackagenames`. [#29](https://github.com/uber/RxDogTag/pull/29) 108 | - We have a website now! https://uber.github.io/RxDogTag 109 | - We've been doing a lot of work to add thorough benchmarks for RxDogTag. A dedicated section can be 110 | found on the website at https://uber.github.io/RxDogTag/benchmark. 111 | 112 | Special thanks to [@emartynov](https://github.com/emartynov) for contributing to this release! 113 | 114 | Version 0.2.0 115 | ------------- 116 | 117 | _2019-05-14_ 118 | 119 | **New: Builder-based API for configuration** 120 | 121 | RxDogTag now uses a builder API for configuration. Currently this includes the existing `ObserverHandler` 122 | and package whitelisting, as well as a new configuration to optionally disable stacktrace annotations. 123 | 124 | ```java 125 | RxDogTag.builder() 126 | .disableAnnotations() 127 | .addObserverHandlers(...) 128 | .addIgnoredPackages(...) 129 | .configureWith(...) // For other custom AutoDispose.Configurers 130 | .install(); 131 | ``` 132 | 133 | Note: No-config `RxDogTag.install()` still exists, but is now just a proxy to 134 | `RxDogTag.builder().install()` 135 | 136 | Breaking changes: 137 | * `ObserverHandler` no longer handles package whitelisting, as this is now moved to 138 | the builder API. 139 | * The AutoDispose artifact now uses the builder API's `configureWith` support, and 140 | `AutoDisposeObserverHandler` is now just a package-private implementation detail. 141 | 142 | ```java 143 | RxDogTag.builder() 144 | .configureWith(AutoDisposeConfigurer::configure) 145 | .install(); 146 | ``` 147 | 148 | **New: Inferred subscribe point is now the first element in the stacktrace.** 149 | 150 | After discussions with others in the community, the inferred subscribe point is now the first element 151 | in the stacktrace for better grouping with crash reporters (which may have otherwise accidentally grouped 152 | the stacktrace header annotations as the "same" crash). This plus some arrow characters means the stacktraces 153 | look like this: 154 | 155 | ``` 156 | io.reactivex.exceptions.OnErrorNotImplementedException: The mapper function returned a null value. 157 | 158 | Caused by: java.lang.NullPointerException: The mapper function returned a null value. 159 | at com.uber.anotherpackage.ReadMeExample.complex(ReadMeExample.java:55) 160 | at [[ ↑↑ Inferred subscribe point ↑↑ ]].(:0) 161 | at [[ ↓↓ Original trace ↓↓ ]].(:0) 162 | at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39) 163 | at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:57) 164 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeOnObserver.onNext(ObservableSubscribeOn.java:58) 165 | at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable.run(ObservableScalarXMap.java:248) 166 | // ... and so on 167 | ``` 168 | 169 | 170 | 171 | Version 0.1.0 172 | ------------- 173 | 174 | _2019-04-09_ 175 | 176 | Initial release! 177 | 178 | -------------------------------------------------------------------------------- /rxdogtag-autodispose/src/test/java/anotherpackage/AutoDisposeObserverHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import static anotherpackage.DogTagTestUtil.getPreviousLineNumber; 19 | import static autodispose2.AutoDispose.autoDisposable; 20 | import static com.google.common.truth.Truth.assertThat; 21 | 22 | import autodispose2.AutoDispose; 23 | import autodispose2.CompletableSubscribeProxy; 24 | import autodispose2.FlowableSubscribeProxy; 25 | import autodispose2.MaybeSubscribeProxy; 26 | import autodispose2.ObservableSubscribeProxy; 27 | import autodispose2.ScopeProvider; 28 | import autodispose2.SingleSubscribeProxy; 29 | import io.reactivex.rxjava3.core.Completable; 30 | import io.reactivex.rxjava3.core.Flowable; 31 | import io.reactivex.rxjava3.core.Maybe; 32 | import io.reactivex.rxjava3.core.Observable; 33 | import io.reactivex.rxjava3.core.Single; 34 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 35 | import org.junit.After; 36 | import org.junit.Before; 37 | import org.junit.Rule; 38 | import org.junit.Test; 39 | import rxdogtag2.RxDogTag; 40 | import rxdogtag2.autodispose2.AutoDisposeConfigurer; 41 | 42 | /** 43 | * NOTE: These tests are a little odd. There are two conditions for them running correctly because 44 | * they verify via inspecting stacktraces. 1. the throwError() method MUST calculate the line number 45 | * immediately after subscribe (on the next line). 2. it must not be in the rxdogtag2 package 46 | * because that is filtered out in stacktrace inspection. 47 | */ 48 | public final class AutoDisposeObserverHandlerTest implements DogTagTest { 49 | 50 | @Rule public RxErrorsRule errorsRule = new RxErrorsRule(); 51 | 52 | @Before 53 | public void setUp() { 54 | RxDogTag.builder().configureWith(AutoDisposeConfigurer::configure).install(); 55 | } 56 | 57 | @After 58 | public void tearDown() { 59 | RxDogTag.reset(); 60 | } 61 | 62 | @Test 63 | public void testObservable_autodispose() { 64 | Exception original = new RuntimeException("Blah"); 65 | assertRewrittenStacktrace( 66 | subscribeError(Observable.error(original).to(autoDisposable(ScopeProvider.UNBOUND))), 67 | original); 68 | } 69 | 70 | @Test 71 | public void testFlowable_autodispose() { 72 | Exception original = new RuntimeException("Blah"); 73 | assertRewrittenStacktrace( 74 | subscribeError(Flowable.error(original).to(autoDisposable(ScopeProvider.UNBOUND))), 75 | original); 76 | } 77 | 78 | @Test 79 | public void testSingle_autodispose() { 80 | Exception original = new RuntimeException("Blah"); 81 | assertRewrittenStacktrace( 82 | subscribeError(Single.error(original).to(autoDisposable(ScopeProvider.UNBOUND))), original); 83 | } 84 | 85 | @Test 86 | public void testMaybe_autodispose() { 87 | Exception original = new RuntimeException("Blah"); 88 | assertRewrittenStacktrace( 89 | subscribeError(Maybe.error(original).to(autoDisposable(ScopeProvider.UNBOUND))), original); 90 | } 91 | 92 | @Test 93 | public void testCompletable_autodispose() { 94 | Exception original = new RuntimeException("Blah"); 95 | assertRewrittenStacktrace( 96 | subscribeError(Completable.error(original).to(autoDisposable(ScopeProvider.UNBOUND))), 97 | original); 98 | } 99 | 100 | /** This tests that the original stacktrace was rewritten with the relevant source information. */ 101 | private void assertRewrittenStacktrace(int expectedLineNumber, Exception original) { 102 | Throwable e = errorsRule.take(); 103 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 104 | assertThat(e).hasMessageThat().isEqualTo(original.getMessage()); 105 | assertThat(e.getStackTrace()).isEmpty(); 106 | Throwable cause = e.getCause(); 107 | assertThat(cause.getStackTrace()[0].getFileName()) 108 | .isEqualTo(getClass().getSimpleName() + ".java"); 109 | assertThat(cause.getStackTrace()[0].getLineNumber()).isEqualTo(expectedLineNumber); 110 | assertThat(cause.getStackTrace()[1].getClassName()) 111 | .isEqualTo(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 112 | assertThat(cause.getStackTrace()[2].getClassName()) 113 | .isEqualTo(RxDogTag.STACK_ELEMENT_TRACE_HEADER); 114 | } 115 | 116 | /** 117 | * This is a weird but necessary tests. We have proguard configurations that depend on these 118 | * packages to have names kept in order for DogTagObservers to work their magic correctly. 119 | * 120 | *

In the event that this test fails, please update the proguard configurations with the new 121 | * package names. You will see something like this in the bundled proguard config. 122 | * 123 | *


124 |    *   -keepnames class com.uber.autodispose.**
125 |    * 
126 | * 127 | *

This should be updated with the new package name. 128 | */ 129 | @Test 130 | public void verifyPackages() { 131 | checkPackage("AutoDispose", "autodispose2", AutoDispose.class.getPackage().getName()); 132 | } 133 | 134 | private void checkPackage(String name, String expectedPackage, String actualPackage) { 135 | if (!actualPackage.equals(expectedPackage)) { 136 | throw new RuntimeException( 137 | "The package name for " 138 | + name 139 | + " has changed. Please " 140 | + "update this test and the proguard -keepnames configuration to properly keep the new " 141 | + "package It was \n\"-keepnames class " 142 | + expectedPackage 143 | + ".**\"\nbut should now " 144 | + "be\n\"-keepnames class " 145 | + actualPackage 146 | + ".**\""); 147 | } 148 | } 149 | 150 | private static int subscribeError(CompletableSubscribeProxy completable) { 151 | completable.subscribe(); 152 | return getPreviousLineNumber(); 153 | } 154 | 155 | private static int subscribeError(ObservableSubscribeProxy observable) { 156 | observable.subscribe(); 157 | return getPreviousLineNumber(); 158 | } 159 | 160 | private static int subscribeError(MaybeSubscribeProxy maybe) { 161 | maybe.subscribe(); 162 | return getPreviousLineNumber(); 163 | } 164 | 165 | private static int subscribeError(SingleSubscribeProxy single) { 166 | single.subscribe(); 167 | return getPreviousLineNumber(); 168 | } 169 | 170 | private static int subscribeError(FlowableSubscribeProxy flowable) { 171 | flowable.subscribe(); 172 | return getPreviousLineNumber(); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /android-benchmark/benchmark/src/androidTest/kotlin/rxdogtag2/androidbenchmark/RxDogTagAndroidPerf.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 Uber Technologies, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package rxdogtag2.androidbenchmark 18 | 19 | import androidx.benchmark.junit4.BenchmarkRule 20 | import androidx.benchmark.junit4.measureRepeated 21 | import androidx.test.filters.LargeTest 22 | import rxdogtag2.RxDogTag 23 | import io.reactivex.rxjava3.core.Flowable 24 | import io.reactivex.rxjava3.core.Observable 25 | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers 26 | import io.reactivex.rxjava3.disposables.Disposable 27 | import io.reactivex.rxjava3.functions.Action 28 | import io.reactivex.rxjava3.functions.Consumer 29 | import io.reactivex.rxjava3.schedulers.Schedulers 30 | import org.junit.After 31 | import org.junit.Before 32 | import org.junit.Rule 33 | import org.junit.Test 34 | import org.junit.runner.RunWith 35 | import org.junit.runners.Parameterized 36 | import org.junit.runners.Parameterized.Parameters 37 | import java.util.concurrent.CountDownLatch 38 | 39 | /** 40 | * Measures the time it takes to both subscribe and consume events. Run this like a normal Android 41 | * instrumentation test on a device. Note that benchmarks should be run on a real device for more 42 | * realistic real-world results. There are further configurations that can be used for the Androidx 43 | * Benchmark library used here, full documentation can be found at 44 | * https://developer.android.com/studio/profile/benchmark.html. 45 | * 46 | * Basic command line usage is just to run `./gradlew :android-benchmark:benchmark:connectedCheck` 47 | */ 48 | @LargeTest 49 | @RunWith(Parameterized::class) 50 | class RxDogTagAndroidPerf( 51 | private val enabled: Boolean, 52 | private val times: Int, 53 | private val guardedDelegateEnabled: Boolean 54 | ) { 55 | 56 | companion object { 57 | @JvmStatic 58 | @Parameters(name = "enabled={0},times={1},guardedDelegateEnabled={2}") 59 | fun data(): List> { 60 | val list = mutableListOf>() 61 | for (enabled in ENABLED) { 62 | for (times in ITERATIONS) { 63 | for (guardedDelegateEnabled in GUARDED_DELEGATE_ENABLED) { 64 | @Suppress("ControlFlowWithEmptyBody") // Readability 65 | if ((!enabled && guardedDelegateEnabled) || (guardedDelegateEnabled && times == 0)) { 66 | // Skip these configurations since they're irrelevant 67 | } else { 68 | list.add(arrayOf(enabled, times, guardedDelegateEnabled)) 69 | } 70 | } 71 | } 72 | } 73 | return list.toList() 74 | } 75 | 76 | private val ITERATIONS = setOf(0, 1, 1_000, 1_000_000) 77 | private val ENABLED = setOf(true, false) 78 | private val GUARDED_DELEGATE_ENABLED = setOf(true, false) 79 | } 80 | 81 | @get:Rule 82 | val benchmarkRule = BenchmarkRule() 83 | 84 | @Before 85 | fun setup() { 86 | Schedulers.shutdown() 87 | Schedulers.start() 88 | if (enabled) { 89 | RxDogTag.builder() 90 | .apply { 91 | if (!guardedDelegateEnabled) { 92 | guardObserverCallbacks(false) 93 | } 94 | } 95 | .install() 96 | } 97 | } 98 | 99 | @After 100 | fun tearDown() { 101 | RxDogTag.reset() 102 | Schedulers.shutdown() 103 | } 104 | 105 | @Test 106 | fun flowable_simple() { 107 | val flowable = flowableInstance(times) 108 | if (times == 0) { 109 | benchmarkRule.measureRepeated { 110 | disposeWithTimingDisabled(flowable.subscribe()) 111 | } 112 | } else { 113 | benchmarkRule.measureRepeated { 114 | latchConsumer { flowable.subscribe(it) } 115 | } 116 | } 117 | } 118 | 119 | @Test 120 | fun observable_simple() { 121 | val observable = observableInstance(times) 122 | if (times == 0) { 123 | benchmarkRule.measureRepeated { 124 | disposeWithTimingDisabled(observable.subscribe()) 125 | } 126 | } else { 127 | benchmarkRule.measureRepeated { 128 | latchConsumer { observable.subscribe(it) } 129 | } 130 | } 131 | } 132 | 133 | @Test 134 | fun flowable_complex() { 135 | val flowable = flowableInstance(times) 136 | .filter { true } 137 | .map { it * 2 } 138 | .subscribeOn(Schedulers.trampoline()) 139 | .observeOn(AndroidSchedulers.mainThread()) 140 | .takeUntil(Flowable.never()) 141 | .ignoreElements() 142 | if (times == 0) { 143 | benchmarkRule.measureRepeated { 144 | disposeWithTimingDisabled(flowable.subscribe()) 145 | } 146 | } else { 147 | benchmarkRule.measureRepeated { 148 | latchAction { flowable.subscribe(it) } 149 | } 150 | } 151 | } 152 | 153 | @Test 154 | fun observable_complex() { 155 | val observable = observableInstance(times) 156 | .filter { true } 157 | .map { it * 2 } 158 | .subscribeOn(Schedulers.trampoline()) 159 | .observeOn(AndroidSchedulers.mainThread()) 160 | .takeUntil(Observable.never()) 161 | .ignoreElements() 162 | if (times == 0) { 163 | benchmarkRule.measureRepeated { 164 | disposeWithTimingDisabled(observable.subscribe()) 165 | } 166 | } else { 167 | benchmarkRule.measureRepeated { 168 | latchAction { observable.subscribe(it) } 169 | } 170 | } 171 | } 172 | 173 | @Suppress("NOTHING_TO_INLINE") // Inlined for test overhead reasons 174 | private inline fun BenchmarkRule.Scope.disposeWithTimingDisabled(disposable: Disposable) { 175 | runWithTimingDisabled { disposable.dispose() } 176 | } 177 | 178 | private inline fun BenchmarkRule.Scope.latchAction(onComplete: (Action) -> Unit) { 179 | val latch = runWithTimingDisabled { CountDownLatch(1) } 180 | onComplete(Action { latch.countDown() }) 181 | latch.await() 182 | } 183 | 184 | private inline fun BenchmarkRule.Scope.latchConsumer(onComplete: (Consumer) -> Unit) { 185 | val latch = runWithTimingDisabled { CountDownLatch(1) } 186 | onComplete(Consumer { latch.countDown() }) 187 | latch.await() 188 | } 189 | 190 | /** 191 | * @param times -1 for empty, 0 for never, otherwise that number of emissions 192 | */ 193 | private fun flowableInstance(times: Int): Flowable { 194 | if (times == 0) { 195 | return Flowable.never() 196 | } else if (times == -1) { 197 | return Flowable.empty() 198 | } 199 | return Flowable.fromArray(*Array(times) { 777 }) 200 | } 201 | 202 | /** 203 | * @param times -1 for empty, 0 for never, otherwise that number of emissions 204 | */ 205 | private fun observableInstance(times: Int): Observable { 206 | return when (times) { 207 | 0 -> Observable.never() 208 | -1 -> Observable.empty() 209 | else -> Observable.fromArray(*Array(times) { 777 }) 210 | } 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RxDogTag 2 | 3 | RxDogTag is a utility to tag originating subscribe points in RxJava 2+ observers, with the goal of 4 | surfacing their subscribe locations for error reporting/investigation later in the event of an unhandled 5 | error. This is _only_ for RxJava observers that do not implement `onError()`. 6 | 7 | ## Download 8 | 9 | If you're targeting RxJava 2: 10 | 11 | [![Maven Central](https://img.shields.io/maven-central/v/com.uber.rxdogtag/rxdogtag.svg)](https://mvnrepository.com/artifact/com.uber.rxdogtag/rxdogtag) 12 | 13 | ```gradle 14 | implementation("com.uber.rxdogtag:rxdogtag:x.y.z") 15 | ``` 16 | 17 | If you're targeting RxJava 3: 18 | 19 | [![Maven Central](https://img.shields.io/maven-central/v/com.uber.rxdogtag2/rxdogtag.svg)](https://mvnrepository.com/artifact/com.uber.rxdogtag2/rxdogtag) 20 | 21 | ```gradle 22 | implementation("com.uber.rxdogtag2:rxdogtag:x.y.z") 23 | ``` 24 | 25 | # Setup 26 | 27 | Install early in your application lifecycle via `RxDogTag.install()`. This will install the necessary 28 | hooks in `RxJavaPlugins`. Note that these will replace any existing plugins at the hooks it uses. See 29 | the [JavaDoc](https://uber.github.io/RxDogTag/1.x/rxdogtag/com.uber.rxdogtag/-rx-dog-tag/install/) for full details of which plugins it uses. 30 | 31 | ## Example 32 | 33 | Consider the following classic RxJava error: 34 | 35 | ```java 36 | Observable.range(0, 10) 37 | .subscribeOn(Schedulers.io()) 38 | .map(i -> null) 39 | .subscribe(); 40 | ``` 41 | 42 | This is a fairly common case in RxJava concurrency. Without tagging, this yields the following trace: 43 | 44 | ``` 45 | io.reactivex.exceptions.OnErrorNotImplementedException: The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | The mapper function returned a null value. 46 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704) 47 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701) 48 | at io.reactivex.internal.observers.LambdaObserver.onError(LambdaObserver.java:77) 49 | at io.reactivex.internal.observers.BasicFuseableObserver.onError(BasicFuseableObserver.java:100) 50 | at io.reactivex.internal.observers.BasicFuseableObserver.fail(BasicFuseableObserver.java:110) 51 | at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:59) 52 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeOnObserver.onNext(ObservableSubscribeOn.java:58) 53 | at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable.run(ObservableScalarXMap.java:248) 54 | at io.reactivex.internal.operators.observable.ObservableJust.subscribeActual(ObservableJust.java:35) 55 | at io.reactivex.Observable.subscribe(Observable.java:12090) 56 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96) 57 | at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578) 58 | at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66) 59 | at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57) 60 | at java.util.concurrent.FutureTask.run(FutureTask.java:266) 61 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) 62 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) 63 | at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 64 | at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 65 | at java.lang.Thread.run(Thread.java:748) 66 | Caused by: java.lang.NullPointerException: The mapper function returned a null value. 67 | at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39) 68 | at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:57) 69 | ... 14 more 70 | ``` 71 | 72 | This is basically impossible to investigate if you're looking at a crash report from the wild. 73 | 74 | Now the same error with RxDogTag enabled: 75 | 76 | ``` 77 | io.reactivex.exceptions.OnErrorNotImplementedException: The mapper function returned a null value. 78 | 79 | Caused by: java.lang.NullPointerException: The mapper function returned a null value. 80 | at anotherpackage.ReadMeExample.complex(ReadMeExample.java:55) 81 | at [[ ↑↑ Inferred subscribe point ↑↑ ]].(:0) 82 | at [[ ↓↓ Original trace ↓↓ ]].(:0) 83 | at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39) 84 | at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:57) 85 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeOnObserver.onNext(ObservableSubscribeOn.java:58) 86 | at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable.run(ObservableScalarXMap.java:248) 87 | at io.reactivex.internal.operators.observable.ObservableJust.subscribeActual(ObservableJust.java:35) 88 | at io.reactivex.Observable.subscribe(Observable.java:12090) 89 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96) 90 | at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578) 91 | at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66) 92 | at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57) 93 | at java.util.concurrent.FutureTask.run(FutureTask.java:266) 94 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) 95 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) 96 | at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 97 | at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 98 | at java.lang.Thread.run(Thread.java:748) 99 | ``` 100 | 101 | Now we have the example subscribe line at `ReadMeExample.java:55`. It may not be a silver bullet to 102 | root-causing why the exception occurred, but at least you know where it's emanating from. 103 | 104 | The subscribe line reported should also retrace and group well for crash reporting. As we use our own 105 | in-house reporter though, we're very open to feedback on how this can be improved for other solutions. 106 | 107 | More examples and details can be found in the [wiki](https://github.com/uber/RxDogTag/wiki) 108 | 109 | ## Configuration 110 | 111 | RxDogTag has an alternative `RxDogTag.builder()` API to facilitate added configuration, such as annotation 112 | control, stacktrace element location, and more. 113 | 114 | ### Custom handlers 115 | 116 | In the event of custom observers that possibly decorate other observer types, this information can 117 | be passed to RxDogTag via the `ObserverHandler` interface. This interface can be used to unwrap 118 | these custom observers to reveal their delegates and their potential behavior. Install these via 119 | the `RxDogTag.Builder#addObserverHandlers(...)` overloads that accept handlers. 120 | 121 | ### Ignored packages 122 | 123 | RxDogTag needs to ignore certain packages (such as its own or RxJava's) when inspecting stack traces 124 | to deduce the subscribe point. You can add other custom ones via `RxDogTag.Builder#addIgnoredPackages(...)`. 125 | 126 | ### AutoDispose support 127 | 128 | AutoDispose is a library for automatically disposing streams, and works via its own decorating observers 129 | under the hood. AutoDispose can work with RxDogTag via its `delegateObserver()` APIs on the AutoDisposingObserver 130 | interfaces. Support for this is available via separate `rxdogtag-autodispose` artifact and its 131 | `AutoDisposeObserverHandler` singleton instance. 132 | 133 | ```java 134 | RxDogTag.builder() 135 | .configureWith(AutoDisposeConfigurer::configure) 136 | .install(); 137 | ``` 138 | 139 | If you're targeting RxJava 2: 140 | 141 | [![Maven Central](https://img.shields.io/maven-central/v/com.uber.rxdogtag/rxdogtag-autodispose.svg)](https://mvnrepository.com/artifact/com.uber.rxdogtag/rxdogtag-autodispose) 142 | 143 | ```gradle 144 | implementation("com.uber.rxdogtag:rxdogtag-autodispose:x.y.z") 145 | ``` 146 | 147 | If you're targeting RxJava 3: 148 | 149 | [![Maven Central](https://img.shields.io/maven-central/v/com.uber.rxdogtag2/rxdogtag-autodispose.svg)](https://mvnrepository.com/artifact/com.uber.rxdogtag2/rxdogtag-autodispose) 150 | 151 | ```gradle 152 | implementation("com.uber.rxdogtag2:rxdogtag-autodispose:x.y.z") 153 | ``` 154 | 155 | ## Development 156 | 157 | Javadocs for the most recent release can be found here: https://uber.github.io/RxDogTag/0.x/rxdogtag/com.uber.rxdogtag/ 158 | 159 | Snapshots of the development version are available in [Sonatype's snapshots repository][snapshots]. 160 | 161 | License 162 | ------- 163 | 164 | Copyright (C) 2019 Uber Technologies 165 | 166 | Licensed under the Apache License, Version 2.0 (the "License"); 167 | you may not use this file except in compliance with the License. 168 | You may obtain a copy of the License at 169 | 170 | http://www.apache.org/licenses/LICENSE-2.0 171 | 172 | Unless required by applicable law or agreed to in writing, software 173 | distributed under the License is distributed on an "AS IS" BASIS, 174 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 175 | See the License for the specific language governing permissions and 176 | limitations under the License. 177 | 178 | [snapshots]: https://oss.sonatype.org/content/repositories/snapshots/com/uber/rxdogtag/ 179 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/RxDogTagConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import static com.google.common.truth.Truth.assertThat; 19 | 20 | import io.reactivex.rxjava3.core.Observable; 21 | import io.reactivex.rxjava3.core.Observer; 22 | import io.reactivex.rxjava3.disposables.Disposable; 23 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 24 | import io.reactivex.rxjava3.observers.DisposableObserver; 25 | import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; 26 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 27 | import org.junit.After; 28 | import org.junit.Rule; 29 | import org.junit.Test; 30 | import rxdogtag2.ObserverHandler; 31 | import rxdogtag2.RxDogTag; 32 | 33 | public final class RxDogTagConfigurationTest implements DogTagTest { 34 | 35 | @Rule public RxErrorsRule errorsRule = new RxErrorsRule(); 36 | 37 | @After 38 | public void tearDown() { 39 | RxDogTag.reset(); 40 | } 41 | 42 | @Test 43 | public void installAndResetObservers() { 44 | RxDogTag.install(); 45 | assertThat(RxJavaPlugins.getOnFlowableSubscribe()).isNotNull(); 46 | assertThat(RxJavaPlugins.getOnObservableSubscribe()).isNotNull(); 47 | assertThat(RxJavaPlugins.getOnMaybeSubscribe()).isNotNull(); 48 | assertThat(RxJavaPlugins.getOnSingleSubscribe()).isNotNull(); 49 | assertThat(RxJavaPlugins.getOnCompletableSubscribe()).isNotNull(); 50 | 51 | RxDogTag.reset(); 52 | assertThat(RxJavaPlugins.getOnFlowableSubscribe()).isNull(); 53 | assertThat(RxJavaPlugins.getOnObservableSubscribe()).isNull(); 54 | assertThat(RxJavaPlugins.getOnMaybeSubscribe()).isNull(); 55 | assertThat(RxJavaPlugins.getOnSingleSubscribe()).isNull(); 56 | assertThat(RxJavaPlugins.getOnCompletableSubscribe()).isNull(); 57 | } 58 | 59 | @Test 60 | public void customHandlersGoFirst() { 61 | // A custom handler that hides the upstream observer to demonstrate that custom handlers get 62 | // first pick. 63 | ObserverHandler handler = 64 | new ObserverHandler() { 65 | @Override 66 | public Observer handle(Observable observable, Observer observer) { 67 | return new LambdaConsumerObserver() { 68 | @Override 69 | public void onSubscribe(Disposable d) { 70 | observer.onSubscribe(d); 71 | } 72 | 73 | @Override 74 | public void onNext(Object o) { 75 | //noinspection unchecked 76 | observer.onNext(o); 77 | } 78 | 79 | @Override 80 | public void onError(Throwable e) { 81 | RxJavaPlugins.onError(e); 82 | } 83 | 84 | @Override 85 | public void onComplete() { 86 | observer.onComplete(); 87 | } 88 | 89 | @Override 90 | public boolean hasCustomOnError() { 91 | return false; 92 | } 93 | }; 94 | } 95 | }; 96 | 97 | RxDogTag.builder().addObserverHandlers(handler).install(); 98 | Exception expected = new RuntimeException("Exception!"); 99 | Observable.error(expected) 100 | .subscribe( 101 | new DisposableObserver() { 102 | @Override 103 | public void onNext(Object o) {} 104 | 105 | @Override 106 | public void onError(Throwable e) { 107 | throw new RuntimeException("Another exception"); 108 | } 109 | 110 | @Override 111 | public void onComplete() {} 112 | }); 113 | 114 | Throwable e = errorsRule.take(); 115 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 116 | assertThat(e.getStackTrace()).isEmpty(); 117 | Throwable cause = e.getCause(); 118 | assertThat(cause).isSameInstanceAs(expected); 119 | assertThat(cause.getStackTrace()[1].toString()).contains(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 120 | } 121 | 122 | @Test 123 | public void customHandlersAreSkippedPastIfNoHandling() { 124 | // A custom handler that doesn't report a LambdaConsumerIntrospection 125 | ObserverHandler handler = 126 | new ObserverHandler() { 127 | @Override 128 | public Observer handle(Observable observable, Observer observer) { 129 | return observer; 130 | } 131 | }; 132 | 133 | RxDogTag.builder().addObserverHandlers(handler).install(); 134 | Exception expected = new RuntimeException("Exception!"); 135 | Observable.error(expected).subscribe(); 136 | 137 | Throwable e = errorsRule.take(); 138 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 139 | assertThat(e.getStackTrace()).isEmpty(); 140 | Throwable cause = e.getCause(); 141 | assertThat(cause).isSameInstanceAs(expected); 142 | assertThat(cause.getStackTrace()[1].toString()).contains(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 143 | } 144 | 145 | @Test 146 | public void customPackages() { 147 | String thisPackage = getClass().getPackage().getName(); 148 | 149 | RxDogTag.builder().addIgnoredPackages(thisPackage).install(); 150 | Exception expected = new RuntimeException("Exception!"); 151 | Observable.error(expected).subscribe(); 152 | 153 | Throwable e = errorsRule.take(); 154 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 155 | assertThat(e.getStackTrace()).isEmpty(); 156 | Throwable cause = e.getCause(); 157 | assertThat(cause).isSameInstanceAs(expected); 158 | assertThat(cause.getStackTrace()[1].toString()).contains(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 159 | 160 | // Confirm that we ignored the subscribe line in this test because of the matching package. 161 | assertThat(cause.getStackTrace()[1].toString().startsWith(thisPackage)).isFalse(); 162 | } 163 | 164 | @Test 165 | public void disableAnnotations() { 166 | RxDogTag.builder().disableAnnotations().install(); 167 | Exception expected = new RuntimeException("Exception!"); 168 | Observable.error(expected).subscribe(); 169 | 170 | Throwable e = errorsRule.take(); 171 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 172 | assertThat(e.getStackTrace()).isEmpty(); 173 | Throwable cause = e.getCause(); 174 | assertThat(cause).isSameInstanceAs(expected); 175 | assertThat(cause.getStackTrace()[1].toString()) 176 | .doesNotContain(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 177 | } 178 | 179 | @Test 180 | public void repackagingOENIEs_onByDefault() { 181 | RxDogTag.builder().install(); 182 | Exception expected = new RuntimeException("Exception!"); 183 | OnErrorNotImplementedException parent = new OnErrorNotImplementedException(expected); 184 | Observable.error(parent).subscribe(); 185 | 186 | Throwable e = errorsRule.take(); 187 | 188 | // Confirm it's repackaged 189 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 190 | assertThat(e.getStackTrace()).isEmpty(); 191 | assertThat(e).hasMessageThat().isEqualTo(expected.getMessage()); 192 | Throwable cause = e.getCause(); 193 | assertThat(cause).isSameInstanceAs(expected); 194 | assertThat(cause.getStackTrace()[1].toString()).contains(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 195 | } 196 | 197 | @Test 198 | public void repackagingOENIEs_disabled() { 199 | RxDogTag.builder().disableRepackagingOnErrorNotImplementedExceptions().install(); 200 | Exception expected = new RuntimeException("Exception!"); 201 | OnErrorNotImplementedException parent = new OnErrorNotImplementedException(expected); 202 | Observable.error(parent).subscribe(); 203 | 204 | Throwable e = errorsRule.take(); 205 | 206 | // Assert it's the original, untouched exception 207 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 208 | assertThat(e).isSameInstanceAs(parent); 209 | assertThat(e.getStackTrace()).isEqualTo(parent.getStackTrace()); 210 | assertThat(e).hasMessageThat().isEqualTo(parent.getMessage()); 211 | Throwable cause = e.getCause(); 212 | assertThat(cause).isSameInstanceAs(expected); 213 | assertThat(cause.getStackTrace()[1].toString()).contains(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 214 | } 215 | 216 | @Test 217 | public void disableGuardObserverChecks_rewritesStacktrace() { 218 | RxDogTag.builder().guardObserverCallbacks(false).install(); 219 | 220 | Exception original = new RuntimeException("Exception!"); 221 | Observable.error(original).subscribe(); 222 | 223 | Throwable e = errorsRule.take(); 224 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 225 | assertThat(e).hasMessageThat().isEqualTo(original.getMessage()); 226 | assertThat(e.getStackTrace()).isEmpty(); 227 | Throwable cause = e.getCause(); 228 | assertThat(cause.getStackTrace()[0].getFileName()) 229 | .isEqualTo(getClass().getSimpleName() + ".java"); 230 | assertThat(cause.getStackTrace()[1].getClassName()) 231 | .isEqualTo(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 232 | assertThat(cause.getStackTrace()[2].getClassName()) 233 | .isEqualTo(RxDogTag.STACK_ELEMENT_TRACE_HEADER); 234 | } 235 | 236 | abstract static class LambdaConsumerObserver 237 | implements Observer, LambdaConsumerIntrospection {} 238 | } 239 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /android-benchmark/benchmark/src/main/kotlin/rxdogtag2/androidbenchmark/DataParser.kt: -------------------------------------------------------------------------------- 1 | package rxdogtag2.androidbenchmark 2 | 3 | import java.util.Locale 4 | 5 | fun main() { 6 | 7 | val data = """ 8 | benchmark: 9,770 ns RxDogTagAndroidPerf.observable_complex[enabled=true,times=0,guardedDelegateEnabled=false] 9 | benchmark: 20,783 ns RxDogTagAndroidPerf.flowable_complex[enabled=true,times=0,guardedDelegateEnabled=false] 10 | benchmark: 7,506 ns RxDogTagAndroidPerf.flowable_simple[enabled=true,times=0,guardedDelegateEnabled=false] 11 | benchmark: 7,275 ns RxDogTagAndroidPerf.observable_simple[enabled=true,times=0,guardedDelegateEnabled=false] 12 | benchmark: 50,492 ns RxDogTagAndroidPerf.observable_complex[enabled=true,times=1,guardedDelegateEnabled=true] 13 | benchmark: 50,787 ns RxDogTagAndroidPerf.flowable_complex[enabled=true,times=1,guardedDelegateEnabled=true] 14 | benchmark: 7,581 ns RxDogTagAndroidPerf.flowable_simple[enabled=true,times=1,guardedDelegateEnabled=true] 15 | benchmark: 7,378 ns RxDogTagAndroidPerf.observable_simple[enabled=true,times=1,guardedDelegateEnabled=true] 16 | benchmark: 49,195 ns RxDogTagAndroidPerf.observable_complex[enabled=true,times=1,guardedDelegateEnabled=false] 17 | benchmark: 50,081 ns RxDogTagAndroidPerf.flowable_complex[enabled=true,times=1,guardedDelegateEnabled=false] 18 | benchmark: 7,234 ns RxDogTagAndroidPerf.flowable_simple[enabled=true,times=1,guardedDelegateEnabled=false] 19 | benchmark: 7,060 ns RxDogTagAndroidPerf.observable_simple[enabled=true,times=1,guardedDelegateEnabled=false] 20 | benchmark: 313,334 ns RxDogTagAndroidPerf.observable_complex[enabled=true,times=1,000,guardedDelegateEnabled=true] 21 | benchmark: 342,812 ns RxDogTagAndroidPerf.flowable_complex[enabled=true,times=1,000,guardedDelegateEnabled=true] 22 | benchmark: 153,124 ns RxDogTagAndroidPerf.flowable_simple[enabled=true,times=1,000,guardedDelegateEnabled=true] 23 | benchmark: 152,943 ns RxDogTagAndroidPerf.observable_simple[enabled=true,times=1,000,guardedDelegateEnabled=true] 24 | benchmark: 312,864 ns RxDogTagAndroidPerf.observable_complex[enabled=true,times=1,000,guardedDelegateEnabled=false] 25 | benchmark: 375,208 ns RxDogTagAndroidPerf.flowable_complex[enabled=true,times=1,000,guardedDelegateEnabled=false] 26 | benchmark: 40,568 ns RxDogTagAndroidPerf.flowable_simple[enabled=true,times=1,000,guardedDelegateEnabled=false] 27 | benchmark: 40,743 ns RxDogTagAndroidPerf.observable_simple[enabled=true,times=1,000,guardedDelegateEnabled=false] 28 | benchmark: 257,553,671 ns RxDogTagAndroidPerf.observable_complex[enabled=true,times=1,000,000,guardedDelegateEnabled=true] 29 | benchmark: 300,186,228 ns RxDogTagAndroidPerf.flowable_complex[enabled=true,times=1,000,000,guardedDelegateEnabled=true] 30 | benchmark: 162,547,359 ns RxDogTagAndroidPerf.flowable_simple[enabled=true,times=1,000,000,guardedDelegateEnabled=true] 31 | benchmark: 166,887,047 ns RxDogTagAndroidPerf.observable_simple[enabled=true,times=1,000,000,guardedDelegateEnabled=true] 32 | benchmark: 249,739,764 ns RxDogTagAndroidPerf.observable_complex[enabled=true,times=1,000,000,guardedDelegateEnabled=false] 33 | benchmark: 302,880,498 ns RxDogTagAndroidPerf.flowable_complex[enabled=true,times=1,000,000,guardedDelegateEnabled=false] 34 | benchmark: 26,791,825 ns RxDogTagAndroidPerf.flowable_simple[enabled=true,times=1,000,000,guardedDelegateEnabled=false] 35 | benchmark: 27,304,847 ns RxDogTagAndroidPerf.observable_simple[enabled=true,times=1,000,000,guardedDelegateEnabled=false] 36 | benchmark: 1,673 ns RxDogTagAndroidPerf.observable_complex[enabled=false,times=0,guardedDelegateEnabled=false] 37 | benchmark: 5,741 ns RxDogTagAndroidPerf.flowable_complex[enabled=false,times=0,guardedDelegateEnabled=false] 38 | benchmark: 365 ns RxDogTagAndroidPerf.flowable_simple[enabled=false,times=0,guardedDelegateEnabled=false] 39 | benchmark: 331 ns RxDogTagAndroidPerf.observable_simple[enabled=false,times=0,guardedDelegateEnabled=false] 40 | benchmark: 53,672 ns RxDogTagAndroidPerf.observable_complex[enabled=false,times=1,guardedDelegateEnabled=false] 41 | benchmark: 56,004 ns RxDogTagAndroidPerf.flowable_complex[enabled=false,times=1,guardedDelegateEnabled=false] 42 | benchmark: 519 ns RxDogTagAndroidPerf.flowable_simple[enabled=false,times=1,guardedDelegateEnabled=false] 43 | benchmark: 532 ns RxDogTagAndroidPerf.observable_simple[enabled=false,times=1,guardedDelegateEnabled=false] 44 | benchmark: 291,301 ns RxDogTagAndroidPerf.observable_complex[enabled=false,times=1,000,guardedDelegateEnabled=false] 45 | benchmark: 273,178 ns RxDogTagAndroidPerf.flowable_complex[enabled=false,times=1,000,guardedDelegateEnabled=false] 46 | benchmark: 24,920 ns RxDogTagAndroidPerf.flowable_simple[enabled=false,times=1,000,guardedDelegateEnabled=false] 47 | benchmark: 23,968 ns RxDogTagAndroidPerf.observable_simple[enabled=false,times=1,000,guardedDelegateEnabled=false] 48 | benchmark: 252,727,577 ns RxDogTagAndroidPerf.observable_complex[enabled=false,times=1,000,000,guardedDelegateEnabled=false] 49 | benchmark: 304,952,062 ns RxDogTagAndroidPerf.flowable_complex[enabled=false,times=1,000,000,guardedDelegateEnabled=false] 50 | benchmark: 23,952,919 ns RxDogTagAndroidPerf.flowable_simple[enabled=false,times=1,000,000,guardedDelegateEnabled=false] 51 | benchmark: 23,993,700 ns RxDogTagAndroidPerf.observable_simple[enabled=false,times=1,000,000,guardedDelegateEnabled=false] 52 | """.trimIndent() 53 | 54 | // Skip the header line 55 | val results = data.lineSequence() 56 | .filterNot { it.isBlank() } 57 | .filter { it.startsWith("benchmark") } 58 | .map { line -> 59 | // benchmark: 6,154,949 ns SpeedTest.gson_autovalue_buffer_fromJson_minified 60 | val (_, score, units, benchmark) = line.split("\\s+".toRegex()) 61 | Analysis( 62 | benchmark = benchmark, 63 | score = score.replace(",", "").toLong(), 64 | units = units 65 | ) 66 | } 67 | .toList() 68 | 69 | ResultType.values().forEach { printResults(it, results) } 70 | } 71 | 72 | private fun printResults(type: ResultType, results: List) { 73 | val groupedResults = type.groupings.associateWith { grouping -> 74 | results.filter { 75 | grouping.matchFunction(it.benchmark) 76 | } 77 | } 78 | check(groupedResults.isNotEmpty()) { 79 | "Empty results for $type with \n$results" 80 | } 81 | val scoreLength = results.maxBy { it.formattedScore.length }!!.formattedScore.length 82 | 83 | val output = buildString { 84 | appendln() 85 | append("## ") 86 | append(type.title) 87 | appendln() 88 | appendln() 89 | append("_") 90 | append(type.description) 91 | append("_") 92 | appendln() 93 | appendln() 94 | groupedResults.entries 95 | .joinTo(this, "\n\n", postfix = "\n") { (grouping, matchedAnalyses) -> 96 | check(matchedAnalyses.isNotEmpty()) { 97 | "Empty analysis for $type, $grouping, with \n$matchedAnalyses" 98 | } 99 | val sorted = matchedAnalyses.sortedBy { it.score } 100 | val first = sorted[0] 101 | val largestDelta = sorted.drop(1) 102 | .map { 103 | val delta = ((it.score - first.score).toDouble() / first.score) * 100 104 | String.format(Locale.US, "%.2f", delta) 105 | } 106 | .maxBy { 107 | it.length 108 | }!! 109 | .length 110 | val content = sorted 111 | .joinToString("\n") { analysis -> 112 | analysis.formattedString( 113 | scoreLength, 114 | largestDelta 115 | ) 116 | } 117 | "### ${grouping.name}" + 118 | "\n| RxDogTag Enabled | Guarded Observer Callbacks Enabled | Time (ms) | Time (ns) |" + 119 | "\n|----------|----------|------------|-----------|" + 120 | "\n$content" 121 | } 122 | } 123 | 124 | println(output) 125 | } 126 | 127 | private fun String.isFlowable(): Boolean = "flowable" in this 128 | private fun String.isObservable(): Boolean = "observable" in this 129 | private fun String.isSubscribeThroughput(): Boolean = "times=0" in this 130 | private fun String.isSingle(): Boolean = "=1,g" in this 131 | private fun String.isThousand(): Boolean = "=1,000,g" in this 132 | private fun String.isMillion(): Boolean = "=1,000,000,g" in this 133 | private fun String.isSimple(): Boolean = "simple" in this 134 | private fun String.isComplex(): Boolean = "complex" in this 135 | 136 | private enum class ResultType(val title: String, val description: String, val groupings: List) { 137 | THROUGHPUT( 138 | title = "Event throughput: grouped by number of events", 139 | description = "Measures the amount of time it takes for given number of elements to pass through the stream.", 140 | groupings = run { 141 | mutableListOf().apply { 142 | val complexities: Map Boolean> = mapOf( 143 | "Simple" to String::isSimple, 144 | "Complex" to String::isComplex 145 | ) 146 | val counts: Map Boolean> = mapOf( 147 | "1 item" to String::isSingle, 148 | "1_000 items" to String::isThousand, 149 | "1_000_000 items" to String::isMillion 150 | ) 151 | val types: Map Boolean> = mapOf( 152 | "Observable" to String::isObservable, 153 | "Flowable" to String::isFlowable 154 | ) 155 | for (type in types) { 156 | for (count in counts) { 157 | for (complexity in complexities) { 158 | add(Grouping("${complexity.key}: ${count.key} (${type.key})") { 159 | complexity.value.invoke(it) && count.value.invoke(it) && type.value.invoke(it) 160 | }) 161 | } 162 | } 163 | } 164 | } 165 | } 166 | ), 167 | SUBSCRIBE( 168 | title = "Subscribe cost: grouped by complexity", 169 | description = "Measures the cost to subscription incurred by RxDogTag. Subscription means no emissions, subscription only.", 170 | groupings = listOf( 171 | Grouping("Simple (Observable)") { 172 | it.isSimple() && it.isSubscribeThroughput() && it.isObservable() 173 | }, 174 | Grouping("Simple (Flowable)") { 175 | it.isSimple() && it.isSubscribeThroughput() && it.isFlowable() 176 | }, 177 | Grouping("Complex (Observable)") { 178 | it.isComplex() && it.isSubscribeThroughput() && it.isObservable() 179 | }, 180 | Grouping("Complex (Flowable)") { 181 | it.isComplex() && it.isSubscribeThroughput() && it.isFlowable() 182 | } 183 | ) 184 | ) 185 | } 186 | 187 | private data class Grouping( 188 | val name: String, 189 | val matchFunction: (String) -> Boolean 190 | ) 191 | 192 | private data class Analysis( 193 | val benchmark: String, 194 | val score: Long, 195 | val units: String 196 | ) { 197 | override fun toString() = "$benchmark\t$score\t$units" 198 | 199 | val rxDogTagEnabled = benchmark.substringAfter("enabled=") 200 | .substringBefore(",") 201 | .toBoolean() 202 | val guardedObserverCallbacksEnabled = benchmark.substringAfter("guardedDelegateEnabled=") 203 | .substringBefore("]") 204 | .toBoolean() 205 | 206 | fun formattedString(scoreLength: Int, msLength: Int): String { 207 | return String.format(Locale.US, 208 | "| %-${5}s | %-${5}s | %$msLength.3f%s | %${scoreLength}s%s |", 209 | rxDogTagEnabled, 210 | guardedObserverCallbacksEnabled, 211 | score.toFloat() / 1000000, 212 | "ms", 213 | formattedScore, 214 | units 215 | ) 216 | } 217 | 218 | val formattedScore: String 219 | get() = String.format(Locale.US, "%,d", score) 220 | } 221 | 222 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | ### Simple 4 | 5 | Consider the following trivial case of emitting an error with 6 | no error handling. 7 | 8 | ```java 9 | Observable.error(new RuntimeException("Unhandled error!")) 10 | .subscribe(); 11 | ``` 12 | 13 | This results in a stacktrace like this: 14 | 15 | ```jvm 16 | io.reactivex.exceptions.OnErrorNotImplementedException: The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | Unhandled error! 17 | 18 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704) 19 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701) 20 | at io.reactivex.internal.observers.LambdaObserver.onError(LambdaObserver.java:77) 21 | at io.reactivex.internal.disposables.EmptyDisposable.error(EmptyDisposable.java:63) 22 | at io.reactivex.internal.operators.observable.ObservableError.subscribeActual(ObservableError.java:38) 23 | at io.reactivex.Observable.subscribe(Observable.java:12090) 24 | at io.reactivex.Observable.subscribe(Observable.java:12076) 25 | at io.reactivex.Observable.subscribe(Observable.java:11954) 26 | at anotherpackage.ReadMeExample.simpleSubscribe(ReadMeExample.java:26) 27 | 28 | Caused by: java.lang.RuntimeException: Unhandled error! 29 | at anotherpackage.ReadMeExample.simpleSubscribe(ReadMeExample.java:25) 30 | ... 25 more 31 | ``` 32 | 33 | Now let's look at the same example with tagging enabled: 34 | 35 | ``` 36 | io.reactivex.exceptions.OnErrorNotImplementedException: Unhandled error! 37 | 38 | Caused by: java.lang.RuntimeException: Unhandled error! 39 | at anotherpackage.ReadMeExample.simpleSubscribe(ReadMeExample.java:26) 40 | at [[ ↑↑ Inferred subscribe point ↑↑ ]].(:0) 41 | at [[ ↓↓ Original trace ↓↓ ]].(:0) 42 | at anotherpackage.ReadMeExample.simpleSubscribe(ReadMeExample.java:25) 43 | ... 25 more 44 | ``` 45 | 46 | In the simple case, there's not much added benefit since the execution was synchronous and single threaded, 47 | but you can see original trace has now been updated to indicate the exact line that `subscribe()` was called. 48 | In this case: `ReadMeExample.java:26`. 49 | 50 | To reduce noise - RxDogTag will wrap the original cause in a synthetic 51 | `OnErrorNotImplementedException`. This uses the original cause's message and doesn't fill in the 52 | stacktrace as it's irrelevant to the trace. 53 | 54 | ### Threading 55 | 56 | Consider a more complex example with threading: 57 | 58 | ```java 59 | Observable.just(1) 60 | .subscribeOn(Schedulers.io()) 61 | .map(i -> null) 62 | .subscribe(); 63 | ``` 64 | 65 | This is a fairly common case in RxJava concurrency. Without tagging, this yields the following trace: 66 | 67 | ``` 68 | io.reactivex.exceptions.OnErrorNotImplementedException: The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | The mapper function returned a null value. 69 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704) 70 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701) 71 | at io.reactivex.internal.observers.LambdaObserver.onError(LambdaObserver.java:77) 72 | at io.reactivex.internal.observers.BasicFuseableObserver.onError(BasicFuseableObserver.java:100) 73 | at io.reactivex.internal.observers.BasicFuseableObserver.fail(BasicFuseableObserver.java:110) 74 | at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:59) 75 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeOnObserver.onNext(ObservableSubscribeOn.java:58) 76 | at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable.run(ObservableScalarXMap.java:248) 77 | at io.reactivex.internal.operators.observable.ObservableJust.subscribeActual(ObservableJust.java:35) 78 | at io.reactivex.Observable.subscribe(Observable.java:12090) 79 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96) 80 | at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578) 81 | at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66) 82 | at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57) 83 | at java.util.concurrent.FutureTask.run(FutureTask.java:266) 84 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) 85 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) 86 | at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 87 | at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 88 | at java.lang.Thread.run(Thread.java:748) 89 | Caused by: java.lang.NullPointerException: The mapper function returned a null value. 90 | at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39) 91 | at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:57) 92 | ... 14 more 93 | ``` 94 | 95 | Yikes! This is basically impossible to investigate if you're looking at a crash report from the wild. 96 | 97 | Now the same trace with tagging enabled: 98 | 99 | ``` 100 | io.reactivex.exceptions.OnErrorNotImplementedException: The mapper function returned a null value. 101 | 102 | Caused by: java.lang.NullPointerException: The mapper function returned a null value. 103 | at anotherpackage.ReadMeExample.complex(ReadMeExample.java:55) 104 | at [[ ↑↑ Inferred subscribe point ↑↑ ]].(:0) 105 | at [[ ↓↓ Original trace ↓↓ ]].(:0) 106 | at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39) 107 | at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:57) 108 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeOnObserver.onNext(ObservableSubscribeOn.java:58) 109 | at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable.run(ObservableScalarXMap.java:248) 110 | at io.reactivex.internal.operators.observable.ObservableJust.subscribeActual(ObservableJust.java:35) 111 | at io.reactivex.Observable.subscribe(Observable.java:12090) 112 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96) 113 | at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578) 114 | at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66) 115 | at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57) 116 | at java.util.concurrent.FutureTask.run(FutureTask.java:266) 117 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) 118 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) 119 | at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 120 | at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 121 | at java.lang.Thread.run(Thread.java:748) 122 | ``` 123 | 124 | Now we have the example subscribe line at `ReadMeExample.java:55`. It may not be a silver bullet to 125 | root-causing why the exception occurred, but at least you know where it's emanating from. 126 | 127 | ### Delegate callbacks 128 | 129 | Let's look at one more example. This is similar to the previous, but instead of the exception occurring 130 | upstream in the chain, the exception occurs in one of the observer callbacks; in this case - `onNext`. 131 | 132 | ```java 133 | Observable.just(1) 134 | .subscribeOn(Schedulers.io()) 135 | .subscribe(i -> throwSomething()); 136 | 137 | private void throwSomething() { 138 | throw new RuntimeException("Unhandled error!"); 139 | } 140 | ``` 141 | 142 | Without tagging, this yields the following trace: 143 | 144 | ``` 145 | io.reactivex.exceptions.OnErrorNotImplementedException: The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | Unhandled error! 146 | 147 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704) 148 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701) 149 | at io.reactivex.internal.observers.LambdaObserver.onError(LambdaObserver.java:77) 150 | at io.reactivex.internal.observers.LambdaObserver.onNext(LambdaObserver.java:67) 151 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeOnObserver.onNext(ObservableSubscribeOn.java:58) 152 | at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable.run(ObservableScalarXMap.java:248) 153 | at io.reactivex.internal.operators.observable.ObservableJust.subscribeActual(ObservableJust.java:35) 154 | at io.reactivex.Observable.subscribe(Observable.java:12090) 155 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96) 156 | at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578) 157 | at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66) 158 | at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57) 159 | at java.util.concurrent.FutureTask.run(FutureTask.java:266) 160 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) 161 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) 162 | at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 163 | at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 164 | at java.lang.Thread.run(Thread.java:748) 165 | Caused by: java.lang.RuntimeException: Unhandled error! 166 | at anotherpackage.ReadMeExample.throwSomething(ReadMeExample.java:68) 167 | at anotherpackage.ReadMeExample.lambda$complexDelegate$1(ReadMeExample.java:63) 168 | at io.reactivex.internal.observers.LambdaObserver.onNext(LambdaObserver.java:63) 169 | ... 14 more 170 | ``` 171 | 172 | Similar to the first example, this isn't terrible to root-cause. `onNext` is throwing a traceable 173 | exception in this trivial case. 174 | 175 | With tagging enabled: 176 | 177 | ``` 178 | io.reactivex.exceptions.OnErrorNotImplementedException: The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | Unhandled error! 179 | 180 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704) 181 | at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701) 182 | at io.reactivex.internal.observers.LambdaObserver.onError(LambdaObserver.java:77) 183 | at io.reactivex.internal.observers.LambdaObserver.onNext(LambdaObserver.java:67) 184 | at rxdogtag2.DogTagObserver.lambda$onNext$3(DogTagObserver.java:53) 185 | at rxdogtag2.RxDogTag.guardedDelegateCall(RxDogTag.java:262) 186 | at rxdogtag2.DogTagObserver.onNext(DogTagObserver.java:53) 187 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeOnObserver.onNext(ObservableSubscribeOn.java:58) 188 | at io.reactivex.internal.operators.observable.ObservableScalarXMap$ScalarDisposable.run(ObservableScalarXMap.java:248) 189 | at io.reactivex.internal.operators.observable.ObservableJust.subscribeActual(ObservableJust.java:35) 190 | at io.reactivex.Observable.subscribe(Observable.java:12090) 191 | at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96) 192 | at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578) 193 | at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66) 194 | at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57) 195 | at java.util.concurrent.FutureTask.run(FutureTask.java:266) 196 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) 197 | at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) 198 | at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 199 | at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 200 | at java.lang.Thread.run(Thread.java:748) 201 | Caused by: java.lang.RuntimeException: Unhandled error! 202 | at anotherpackage.ReadMeExample.complexDelegate(ReadMeExample.java:63) 203 | at [[ ↑↑ Inferred subscribe point ↑↑ ]].(:0) 204 | at [[ Originating callback: onNext ]].(:0) 205 | at [[ ↓↓ Original trace ↓↓ ]].(:0) 206 | at anotherpackage.ReadMeExample.throwSomething(ReadMeExample.java:68) 207 | at anotherpackage.ReadMeExample.lambda$complexDelegate$1(ReadMeExample.java:63) 208 | at io.reactivex.internal.observers.LambdaObserver.onNext(LambdaObserver.java:63) 209 | ... 17 more 210 | ``` 211 | 212 | Now we're given added context that this occurred in the `onNext` callback according to `[[ Originating callback: onNext ]]` 213 | at the `subscribe()` call at `ReadMeExample.java:63`. This callback handling is supported for all the 214 | standard callbacks and will report the correct name for each (`onSuccess`, `onComplete`, etc). -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/DogTagObserverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import static anotherpackage.DogTagTestUtil.getPreviousLineNumber; 19 | import static com.google.common.truth.Truth.assertThat; 20 | import static org.junit.Assert.fail; 21 | 22 | import io.reactivex.rxjava3.core.Completable; 23 | import io.reactivex.rxjava3.core.Flowable; 24 | import io.reactivex.rxjava3.core.Maybe; 25 | import io.reactivex.rxjava3.core.Observable; 26 | import io.reactivex.rxjava3.core.Observer; 27 | import io.reactivex.rxjava3.core.Single; 28 | import io.reactivex.rxjava3.disposables.Disposable; 29 | import io.reactivex.rxjava3.exceptions.CompositeException; 30 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 31 | import io.reactivex.rxjava3.observers.TestObserver; 32 | import io.reactivex.rxjava3.subjects.PublishSubject; 33 | import java.util.Locale; 34 | import java.util.concurrent.atomic.AtomicReference; 35 | import org.junit.After; 36 | import org.junit.Before; 37 | import org.junit.Rule; 38 | import org.junit.Test; 39 | import rxdogtag2.RxDogTag; 40 | import rxdogtag2.RxDogTagErrorReceiver; 41 | import rxdogtag2.RxDogTagTaggedExceptionReceiver; 42 | 43 | /** 44 | * NOTE: These tests are a little odd. There are two conditions for them running correctly because 45 | * they verify via inspecting stacktraces. 1. the throwError() method MUST calculate the line number 46 | * immediately after subscribe (on the next line). 2. it must not be in the rxdogtag2 package 47 | * because that is filtered out in stacktrace inspection. 48 | */ 49 | public class DogTagObserverTest implements DogTagTest { 50 | 51 | @Rule public RxErrorsRule errorsRule = new RxErrorsRule(); 52 | 53 | @Before 54 | public void setUp() { 55 | RxDogTag.install(); 56 | } 57 | 58 | @After 59 | public void tearDown() { 60 | RxDogTag.reset(); 61 | } 62 | 63 | @Test 64 | public void testObservable() { 65 | Exception original = new RuntimeException("Blah"); 66 | assertRewrittenStacktrace(subscribeError(Observable.error(original)), original); 67 | } 68 | 69 | @Test 70 | public void testFlowable() { 71 | Exception original = new RuntimeException("Blah"); 72 | assertRewrittenStacktrace(subscribeError(Flowable.error(original)), original); 73 | } 74 | 75 | @Test 76 | public void testSingle() { 77 | Exception original = new RuntimeException("Blah"); 78 | assertRewrittenStacktrace(subscribeError(Single.error(original)), original); 79 | } 80 | 81 | @Test 82 | public void testMaybe() { 83 | Exception original = new RuntimeException("Blah"); 84 | assertRewrittenStacktrace(subscribeError(Maybe.error(original)), original); 85 | } 86 | 87 | @Test 88 | public void testCompletable() { 89 | Exception original = new RuntimeException("Blah"); 90 | assertRewrittenStacktrace(subscribeError(Completable.error(original)), original); 91 | } 92 | 93 | @Test 94 | public void noConstructor_generatesStringFromStackElement_anonymous() { 95 | Exception originalError = new IllegalStateException("illegal state exception"); 96 | assertRewrittenStacktrace( 97 | throwError(new EmptyObserver() {}, originalError), originalError); 98 | } 99 | 100 | @Test 101 | public void noConstructor_generatesStringFromStackElement_instance() { 102 | Exception originalError = new IllegalStateException("illegal state exception"); 103 | Observer o = new EmptyObserver() {}; 104 | assertRewrittenStacktrace(throwError(o, originalError), originalError); 105 | } 106 | 107 | @Test 108 | public void noConstructor_generatesStringFromStackElement_subclass() { 109 | Exception originalError = new IllegalStateException("illegal state exception"); 110 | Another o = new Another(); 111 | assertRewrittenStacktrace(throwError(o, originalError), originalError); 112 | } 113 | 114 | @Test 115 | public void observerWithErrorHandling_ignoredByRxDogTag() { 116 | Exception originalError = new IllegalStateException("illegal state exception"); 117 | // Test observer which custom error handling. 118 | TestObserver testObserver = Observable.error(originalError).test(); 119 | // No errors intercepted by RxDogTag. 120 | errorsRule.assertNoErrors(); 121 | 122 | testObserver.assertError( 123 | (error) -> { 124 | assertThat(error).isNotInstanceOf(OnErrorNotImplementedException.class); 125 | assertThat(error).hasMessageThat().isEqualTo(originalError.getMessage()); 126 | assertThat(error.getStackTrace()).isNotEmpty(); 127 | return true; 128 | }); 129 | } 130 | 131 | @Test 132 | public void nullOriginErrorMessage_replacedWithEmptyString() { 133 | Exception originalError = new IllegalStateException(); 134 | int expectedLineNumber = subscribeError(Observable.error(originalError)); 135 | 136 | Throwable e = errorsRule.take(); 137 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 138 | // Original error message was null. Now replaced by empty string 139 | assertThat(e).hasMessageThat().isNotEqualTo(originalError.getMessage()); 140 | assertThat(e).hasMessageThat().isEqualTo(""); 141 | 142 | assertThat(e.getStackTrace()).isEmpty(); 143 | Throwable cause = e.getCause(); 144 | assertThat(cause.getStackTrace()[0].getFileName()) 145 | .isEqualTo(getClass().getSimpleName() + ".java"); 146 | assertThat(cause.getStackTrace()[0].getLineNumber()).isEqualTo(expectedLineNumber); 147 | assertThat(cause.getStackTrace()[1].getClassName()) 148 | .isEqualTo(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 149 | assertThat(cause.getStackTrace()[2].getClassName()) 150 | .isEqualTo(RxDogTag.STACK_ELEMENT_TRACE_HEADER); 151 | } 152 | 153 | @Test 154 | public void compositeException() { 155 | IllegalStateException original = new IllegalStateException("Initial exception"); 156 | RuntimeException runtimeException = new RuntimeException("Resulting exception"); 157 | 158 | CompositeException compositeException = new CompositeException(original, runtimeException); 159 | int expectedLineNumber = subscribeError(Observable.error(compositeException)); 160 | 161 | assertRewrittenStacktrace(expectedLineNumber, compositeException); 162 | } 163 | 164 | @Test 165 | public void mangledCompositeException() { 166 | Observable mainObservable = 167 | Observable.concatDelayError( 168 | withError( 169 | Observable.just( 170 | withError( 171 | withError(Observable.just(1), new RuntimeException("1")), 172 | new RuntimeException("2"))), 173 | new RuntimeException("3"))); 174 | 175 | int expectedLineNumber = subscribeError(mainObservable); 176 | Throwable e = errorsRule.take(); 177 | assertThat(e).isInstanceOf(OnErrorNotImplementedException.class); 178 | assertThat(e).hasMessageThat().isEqualTo("2 exceptions occurred. "); // For composite exception 179 | assertThat(e.getStackTrace()).isEmpty(); 180 | Throwable cause = e.getCause(); 181 | assertThat(cause.getStackTrace()[0].getFileName()) 182 | .isEqualTo(getClass().getSimpleName() + ".java"); 183 | assertThat(cause.getStackTrace()[0].getLineNumber()).isEqualTo(expectedLineNumber); 184 | assertThat(cause.getStackTrace()[1].getClassName()) 185 | .isEqualTo(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 186 | assertThat(cause.getStackTrace()[2].getClassName()) 187 | .isEqualTo(RxDogTag.STACK_ELEMENT_TRACE_HEADER); 188 | } 189 | 190 | @Test 191 | public void errorReceiver_noGuards() { 192 | RxDogTag.reset(); 193 | RxDogTag.builder().guardObserverCallbacks(false).install(); 194 | AtomicReference recordedError = new AtomicReference<>(); 195 | TestErrorReceiver o = 196 | new TestErrorReceiver() { 197 | @Override 198 | public void onError(Throwable e) { 199 | recordedError.compareAndSet(null, e); 200 | } 201 | 202 | @Override 203 | public void onSubscribe(Disposable d) {} 204 | 205 | @Override 206 | public void onNext(Integer integer) {} 207 | 208 | @Override 209 | public void onComplete() {} 210 | }; 211 | Exception original = new RuntimeException("Blah"); 212 | Observable.error(original).subscribe(o); 213 | Throwable recorded = recordedError.get(); 214 | assertThat(recorded).isSameInstanceAs(original); 215 | } 216 | 217 | @Test 218 | public void errorReceiver_withGuard_noException() { 219 | RxDogTag.reset(); 220 | RxDogTag.builder().guardObserverCallbacks(true).install(); 221 | AtomicReference recordedError = new AtomicReference<>(); 222 | TestErrorReceiver o = 223 | new TestErrorReceiver() { 224 | @Override 225 | public void onError(Throwable e) { 226 | recordedError.compareAndSet(null, e); 227 | } 228 | 229 | @Override 230 | public void onSubscribe(Disposable d) {} 231 | 232 | @Override 233 | public void onNext(Integer integer) {} 234 | 235 | @Override 236 | public void onComplete() {} 237 | }; 238 | Exception original = new RuntimeException("Blah"); 239 | Observable.error(original).subscribe(o); 240 | Throwable recorded = recordedError.get(); 241 | assertThat(recorded).isSameInstanceAs(original); 242 | } 243 | 244 | @Test 245 | public void errorReceiver_withGuard_withException() { 246 | RxDogTag.reset(); 247 | RxDogTag.builder().guardObserverCallbacks(true).install(); 248 | AtomicReference thrownException = new AtomicReference<>(); 249 | TestErrorReceiver o = 250 | new TestErrorReceiver() { 251 | @Override 252 | public void onError(Throwable e) { 253 | RuntimeException toThrow = new RuntimeException(e); 254 | thrownException.compareAndSet(null, toThrow); 255 | throw toThrow; 256 | } 257 | 258 | @Override 259 | public void onSubscribe(Disposable d) {} 260 | 261 | @Override 262 | public void onNext(Integer integer) {} 263 | 264 | @Override 265 | public void onComplete() {} 266 | }; 267 | Exception original = new RuntimeException("Blah"); 268 | PublishSubject subject = PublishSubject.create(); 269 | // Can't do this one synchronously because it will fail in subscribe(); 270 | subject.subscribe(o); 271 | int lineNumber = getPreviousLineNumber(); 272 | subject.onError(original); 273 | Throwable recorded = errorsRule.take(); 274 | Throwable thrown = thrownException.get(); 275 | if (thrown == null) { 276 | // Partially more descriptive error message, partially to make NullAway happy since it doesn't 277 | // speak truth's isNotNull() assertions 278 | fail("thrown was null! This means the observer's onError was never called"); 279 | return; 280 | } 281 | assertThat(thrown).hasCauseThat().isSameInstanceAs(original); 282 | assertThat(recorded).isInstanceOf(OnErrorNotImplementedException.class); 283 | assertThat(recorded).hasCauseThat().isSameInstanceAs(thrown); 284 | // The original cause was put in this 285 | assertThat(recorded).hasMessageThat().contains("java.lang.RuntimeException: Blah"); 286 | 287 | // Slightly duplicated, but this is a special delegates case for onError 288 | Throwable cause = recorded.getCause(); 289 | assertThat(cause).isNotNull(); 290 | assertThat(cause.getStackTrace()[0].getFileName()) 291 | .isEqualTo(getClass().getSimpleName() + ".java"); 292 | assertThat(cause.getStackTrace()[0].getLineNumber()).isEqualTo(lineNumber); 293 | assertThat(cause.getStackTrace()[1].getClassName()) 294 | .isEqualTo(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 295 | assertThat(cause.getStackTrace()[2].getClassName()) 296 | .isEqualTo(String.format(Locale.US, RxDogTag.STACK_ELEMENT_SOURCE_DELEGATE, "onError")); 297 | assertThat(cause.getStackTrace()[3].getClassName()) 298 | .isEqualTo(RxDogTag.STACK_ELEMENT_TRACE_HEADER); 299 | } 300 | 301 | @Test 302 | public void taggedErrorReceiver() { 303 | AtomicReference recordedError = new AtomicReference<>(); 304 | TestTaggedErrorReceiver o = 305 | new TestTaggedErrorReceiver() { 306 | @Override 307 | public void onError(Throwable e) { 308 | recordedError.compareAndSet(null, e); 309 | } 310 | 311 | @Override 312 | public void onSubscribe(Disposable d) {} 313 | 314 | @Override 315 | public void onNext(Integer integer) {} 316 | 317 | @Override 318 | public void onComplete() {} 319 | }; 320 | Exception original = new RuntimeException("Blah"); 321 | Observable.error(original).subscribe(o); 322 | int lineNumber = getPreviousLineNumber(); 323 | Throwable recorded = recordedError.get(); 324 | if (recorded == null) { 325 | fail("No exception recorded!"); 326 | } else { 327 | assertRewrittenStacktrace(lineNumber, original, recorded); 328 | } 329 | } 330 | 331 | private interface TestTaggedErrorReceiver 332 | extends RxDogTagTaggedExceptionReceiver, Observer {} 333 | 334 | private interface TestErrorReceiver extends RxDogTagErrorReceiver, Observer {} 335 | 336 | /** This tests that the original stacktrace was rewritten with the relevant source information. */ 337 | private void assertRewrittenStacktrace(int expectedLineNumber, Throwable original) { 338 | Throwable e = errorsRule.take(); 339 | assertRewrittenStacktrace(expectedLineNumber, original, e); 340 | } 341 | /** This tests that the original stacktrace was rewritten with the relevant source information. */ 342 | private void assertRewrittenStacktrace( 343 | int expectedLineNumber, Throwable original, Throwable recorded) { 344 | assertThat(recorded).isInstanceOf(OnErrorNotImplementedException.class); 345 | assertThat(recorded).hasMessageThat().isEqualTo(original.getMessage()); 346 | assertThat(recorded.getStackTrace()).isEmpty(); 347 | Throwable cause = recorded.getCause(); 348 | assertThat(cause.getStackTrace()[0].getFileName()) 349 | .isEqualTo(getClass().getSimpleName() + ".java"); 350 | assertThat(cause.getStackTrace()[0].getLineNumber()).isEqualTo(expectedLineNumber); 351 | assertThat(cause.getStackTrace()[1].getClassName()) 352 | .isEqualTo(RxDogTag.STACK_ELEMENT_SOURCE_HEADER); 353 | assertThat(cause.getStackTrace()[2].getClassName()) 354 | .isEqualTo(RxDogTag.STACK_ELEMENT_TRACE_HEADER); 355 | } 356 | 357 | private static int subscribeError(Completable completable) { 358 | completable.subscribe(); 359 | return getPreviousLineNumber(); 360 | } 361 | 362 | private static int subscribeError(Observable observable) { 363 | observable.subscribe(); 364 | return getPreviousLineNumber(); 365 | } 366 | 367 | private static int subscribeError(Maybe maybe) { 368 | maybe.subscribe(); 369 | return getPreviousLineNumber(); 370 | } 371 | 372 | private static int subscribeError(Single single) { 373 | single.subscribe(); 374 | return getPreviousLineNumber(); 375 | } 376 | 377 | private static int subscribeError(Flowable flowable) { 378 | flowable.subscribe(); 379 | return getPreviousLineNumber(); 380 | } 381 | 382 | private static int throwError(Observer observer, Exception error) { 383 | Observable.defer(() -> Observable.error(error)).subscribe(observer); 384 | return getPreviousLineNumber(); 385 | } 386 | 387 | private static Observable withError(Observable source, Exception exception) { 388 | return source.concatWith(Observable.error(exception)); 389 | } 390 | 391 | private static class Another extends EmptyObserver { 392 | 393 | @Override 394 | public void onNext(Object unit) {} 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /rxdogtag/src/test/java/anotherpackage/DogTagObserverDelegatesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019. Uber Technologies 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package anotherpackage; 17 | 18 | import static anotherpackage.DogTagTestUtil.getPreviousLineNumber; 19 | import static com.google.common.truth.Truth.assertThat; 20 | import static org.junit.Assert.fail; 21 | 22 | import com.google.common.collect.Lists; 23 | import io.reactivex.rxjava3.core.Completable; 24 | import io.reactivex.rxjava3.core.CompletableObserver; 25 | import io.reactivex.rxjava3.core.Flowable; 26 | import io.reactivex.rxjava3.core.Maybe; 27 | import io.reactivex.rxjava3.core.MaybeObserver; 28 | import io.reactivex.rxjava3.core.Observable; 29 | import io.reactivex.rxjava3.core.Observer; 30 | import io.reactivex.rxjava3.core.Single; 31 | import io.reactivex.rxjava3.core.SingleObserver; 32 | import io.reactivex.rxjava3.disposables.Disposable; 33 | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; 34 | import io.reactivex.rxjava3.functions.Action; 35 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 36 | import java.util.concurrent.atomic.AtomicReference; 37 | import org.junit.After; 38 | import org.junit.Before; 39 | import org.junit.Test; 40 | import org.junit.runner.RunWith; 41 | import org.junit.runners.Parameterized; 42 | import org.reactivestreams.Subscriber; 43 | import org.reactivestreams.Subscription; 44 | import rxdogtag2.RxDogTag; 45 | 46 | @RunWith(Parameterized.class) 47 | public class DogTagObserverDelegatesTest implements DogTagTest { 48 | 49 | private static final Action EMPTY_ACTION = 50 | () -> { 51 | // Noop 52 | }; 53 | 54 | @Parameterized.Parameters(name = "{0}") 55 | public static Object[] data() { 56 | return new Object[] { 57 | TestParams.create( 58 | TestParamType.SYNCHRONOUS, 59 | Flowable.just(1), 60 | Flowable.never(), 61 | Flowable.empty(), 62 | Observable.just(1), 63 | Observable.never(), 64 | Observable.empty(), 65 | Maybe.just(1), 66 | Maybe.never(), 67 | Maybe.empty(), 68 | Single.just(1), 69 | Single.never(), 70 | Completable.complete(), 71 | Completable.never()), 72 | TestParams.create( 73 | TestParamType.LAZY, 74 | Flowable.fromCallable(() -> 1), 75 | Flowable.never(), 76 | Flowable.fromIterable(Lists.newArrayList()), 77 | Observable.fromCallable(() -> 1), 78 | Observable.never(), 79 | Observable.fromIterable(Lists.newArrayList()), 80 | Maybe.fromCallable(() -> 1), 81 | Maybe.never(), 82 | Maybe.fromAction(EMPTY_ACTION), 83 | Single.fromCallable(() -> 1), 84 | Single.never(), 85 | Completable.fromAction(EMPTY_ACTION), 86 | Completable.never()), 87 | TestParams.create( 88 | TestParamType.DEFERRED, 89 | Flowable.defer(() -> Flowable.fromCallable(() -> 1)), 90 | Flowable.defer(Flowable::never), 91 | Flowable.defer(() -> Flowable.fromIterable(Lists.newArrayList())), 92 | Observable.defer(() -> Observable.fromCallable(() -> 1)), 93 | Observable.defer(Observable::never), 94 | Observable.defer(() -> Observable.fromIterable(Lists.newArrayList())), 95 | Maybe.defer(() -> Maybe.fromCallable(() -> 1)), 96 | Maybe.defer(Maybe::never), 97 | Maybe.defer(() -> Maybe.fromAction(EMPTY_ACTION)), 98 | Single.defer(() -> Single.fromCallable(() -> 1)), 99 | Single.defer(Single::never), 100 | Completable.defer(() -> Completable.fromAction(EMPTY_ACTION)), 101 | Completable.defer(Completable::never)) 102 | }; 103 | } 104 | 105 | private final TestParams testParams; 106 | 107 | // We're still expecting the root uncaught exception handler to get hit, but by the time it 108 | // reaches here it should be the decorated exception. 109 | private AtomicReference ref; 110 | 111 | // We'll simulate an OnErrorNotImplemented exception coming from a delegate, which will happen 112 | // if a delegate hits its internal missing onError first before the DogTagObserver has a chance. 113 | private OnErrorNotImplementedException originalError; 114 | 115 | public DogTagObserverDelegatesTest(TestParams testParams) { 116 | this.testParams = testParams; 117 | } 118 | 119 | @Before 120 | public void setUp() { 121 | RxDogTag.install(); 122 | ref = new AtomicReference<>(); 123 | originalError = new OnErrorNotImplementedException(new RuntimeException("Blah")); 124 | } 125 | 126 | @After 127 | public void tearDown() { 128 | RxDogTag.reset(); 129 | } 130 | 131 | @Test 132 | public void delegateSubscriber_throwingInOnSubscribe_shouldBeGuarded() { 133 | Thread.UncaughtExceptionHandler handler = prepareTest(); 134 | Subscriber throwingObserver = 135 | new EmptySubscriber() { 136 | @Override 137 | public void onSubscribe(Subscription s) { 138 | throw originalError; 139 | } 140 | }; 141 | 142 | testParams.flowableNever.subscribe(throwingObserver); 143 | assertTaggedError(getPreviousLineNumber(), handler, "onSubscribe"); 144 | } 145 | 146 | @Test 147 | public void delegateSubscriber_throwingInOnNext_shouldBeGuarded() { 148 | Thread.UncaughtExceptionHandler handler = prepareTest(); 149 | Subscriber throwingOnNext = 150 | new EmptySubscriber() { 151 | @Override 152 | public void onNext(Integer integer) { 153 | throw originalError; 154 | } 155 | }; 156 | 157 | // For Flowable - a synchronous just() will complete in onSubscribe! 158 | String delegateType = testParams.type == TestParamType.SYNCHRONOUS ? "onSubscribe" : "onNext"; 159 | testParams.flowable.subscribe(throwingOnNext); 160 | assertTaggedError(getPreviousLineNumber(), handler, delegateType); 161 | } 162 | 163 | @Test 164 | public void delegateSubscriber_throwingInOnComplete_shouldBeGuarded() { 165 | Thread.UncaughtExceptionHandler handler = prepareTest(); 166 | Subscriber throwingObserver = 167 | new EmptySubscriber() { 168 | @Override 169 | public void onComplete() { 170 | throw originalError; 171 | } 172 | }; 173 | 174 | testParams.flowableComplete.subscribe(throwingObserver); 175 | assertTaggedError(getPreviousLineNumber(), handler, "onComplete"); 176 | } 177 | 178 | @Test 179 | public void delegateObserver_throwingInOnSubscribe_shouldBeGuarded() { 180 | Thread.UncaughtExceptionHandler handler = prepareTest(); 181 | Observer throwingObserver = 182 | new EmptyObserver() { 183 | @Override 184 | public void onSubscribe(Disposable d) { 185 | throw originalError; 186 | } 187 | }; 188 | 189 | testParams.observableNever.subscribe(throwingObserver); 190 | assertTaggedError(getPreviousLineNumber(), handler, "onSubscribe"); 191 | } 192 | 193 | @Test 194 | public void delegateObserver_throwingInOnNext_shouldBeGuarded() { 195 | Thread.UncaughtExceptionHandler handler = prepareTest(); 196 | Observer throwingObserver = 197 | new EmptyObserver() { 198 | @Override 199 | public void onNext(Integer integer) { 200 | throw originalError; 201 | } 202 | }; 203 | 204 | testParams.observable.subscribe(throwingObserver); 205 | assertTaggedError(getPreviousLineNumber(), handler, "onNext"); 206 | } 207 | 208 | @Test 209 | public void delegateObserver_throwingInOnComplete_shouldBeGuarded() { 210 | Thread.UncaughtExceptionHandler handler = prepareTest(); 211 | Observer throwingObserver = 212 | new EmptyObserver() { 213 | @Override 214 | public void onComplete() { 215 | throw originalError; 216 | } 217 | }; 218 | 219 | testParams.observableComplete.subscribe(throwingObserver); 220 | assertTaggedError(getPreviousLineNumber(), handler, "onComplete"); 221 | } 222 | 223 | @Test 224 | public void delegateSingleObserver_throwingInOnSubscribe_shouldBeGuarded() { 225 | Thread.UncaughtExceptionHandler handler = prepareTest(); 226 | SingleObserver throwingObserver = 227 | new EmptySingleObserver() { 228 | @Override 229 | public void onSubscribe(Disposable d) { 230 | throw originalError; 231 | } 232 | }; 233 | 234 | testParams.singleNever.subscribe(throwingObserver); 235 | assertTaggedError(getPreviousLineNumber(), handler, "onSubscribe"); 236 | } 237 | 238 | @Test 239 | public void delegateSingleObserver_throwingInOnSuccess_shouldBeGuarded() { 240 | Thread.UncaughtExceptionHandler handler = prepareTest(); 241 | SingleObserver throwingObserver = 242 | new EmptySingleObserver() { 243 | @Override 244 | public void onSuccess(Integer integer) { 245 | throw originalError; 246 | } 247 | }; 248 | 249 | testParams.single.subscribe(throwingObserver); 250 | assertTaggedError(getPreviousLineNumber(), handler, "onSuccess"); 251 | } 252 | 253 | @Test 254 | public void delegateMaybeObserver_throwingInOnSubscribe_shouldBeGuarded() { 255 | Thread.UncaughtExceptionHandler handler = prepareTest(); 256 | MaybeObserver throwingObserver = 257 | new EmptyMaybeObserver() { 258 | @Override 259 | public void onSubscribe(Disposable d) { 260 | throw originalError; 261 | } 262 | }; 263 | 264 | testParams.maybeNever.subscribe(throwingObserver); 265 | assertTaggedError(getPreviousLineNumber(), handler, "onSubscribe"); 266 | } 267 | 268 | @Test 269 | public void delegateMaybeObserver_throwingInOnSuccess_shouldBeGuarded() { 270 | Thread.UncaughtExceptionHandler handler = prepareTest(); 271 | MaybeObserver throwingObserver = 272 | new EmptyMaybeObserver() { 273 | @Override 274 | public void onSuccess(Integer integer) { 275 | throw originalError; 276 | } 277 | }; 278 | 279 | testParams.maybe.subscribe(throwingObserver); 280 | assertTaggedError(getPreviousLineNumber(), handler, "onSuccess"); 281 | } 282 | 283 | @Test 284 | public void delegateMaybeObserver_throwingInOnComplete_shouldBeGuarded() { 285 | Thread.UncaughtExceptionHandler handler = prepareTest(); 286 | MaybeObserver throwingObserver = 287 | new EmptyMaybeObserver() { 288 | @Override 289 | public void onComplete() { 290 | throw originalError; 291 | } 292 | }; 293 | 294 | testParams.maybeComplete.subscribe(throwingObserver); 295 | assertTaggedError(getPreviousLineNumber(), handler, "onComplete"); 296 | } 297 | 298 | @Test 299 | public void delegateCompletableObserver_throwingInOnSubscribe_shouldBeGuarded() { 300 | Thread.UncaughtExceptionHandler handler = prepareTest(); 301 | CompletableObserver throwingObserver = 302 | new EmptyCompletableObserver() { 303 | @Override 304 | public void onSubscribe(Disposable d) { 305 | throw originalError; 306 | } 307 | }; 308 | 309 | testParams.completableNever.subscribe(throwingObserver); 310 | assertTaggedError(getPreviousLineNumber(), handler, "onSubscribe"); 311 | } 312 | 313 | @Test 314 | public void delegateCompletableObserver_throwingInOnComplete_shouldBeGuarded() { 315 | Thread.UncaughtExceptionHandler handler = prepareTest(); 316 | CompletableObserver throwingObserver = 317 | new EmptyCompletableObserver() { 318 | @Override 319 | public void onComplete() { 320 | throw originalError; 321 | } 322 | }; 323 | 324 | testParams.completable.subscribe(throwingObserver); 325 | assertTaggedError(getPreviousLineNumber(), handler, "onComplete"); 326 | } 327 | 328 | @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/301 329 | private void assertTaggedError( 330 | int lineNumber, Thread.UncaughtExceptionHandler handler, String delegateType) { 331 | Throwable recordedThrowable = ref.get(); 332 | assertThat(recordedThrowable).isNotNull(); 333 | assertUnwrappedError(recordedThrowable, lineNumber, originalError, delegateType); 334 | 335 | // Make sure we've restored the original handler 336 | assertThat(Thread.currentThread().getUncaughtExceptionHandler()).isSameInstanceAs(handler); 337 | } 338 | 339 | private Thread.UncaughtExceptionHandler prepareTest() { 340 | // Taking back control from the RxErrorsRule for this test 341 | RxJavaPlugins.setErrorHandler(null); 342 | 343 | Thread.UncaughtExceptionHandler handler = 344 | (t, e) -> { 345 | if (ref.compareAndSet(null, e)) { 346 | return; 347 | } 348 | // This should not be hit a second time 349 | fail("This shouldn't be hit a second time. " + e); 350 | }; 351 | Thread.currentThread().setUncaughtExceptionHandler(handler); 352 | return handler; 353 | } 354 | 355 | enum TestParamType { 356 | SYNCHRONOUS, 357 | LAZY, 358 | DEFERRED 359 | } 360 | 361 | static class TestParams { 362 | 363 | static TestParams create( 364 | TestParamType type, 365 | Flowable flowable, 366 | Flowable flowableNever, 367 | Flowable flowableComplete, 368 | Observable observable, 369 | Observable observableNever, 370 | Observable observableComplete, 371 | Maybe maybe, 372 | Maybe maybeNever, 373 | Maybe maybeComplete, 374 | Single single, 375 | Single singleNever, 376 | Completable completable, 377 | Completable completableNever) { 378 | return new TestParams( 379 | type, 380 | flowable, 381 | flowableNever, 382 | flowableComplete, 383 | observable, 384 | observableNever, 385 | observableComplete, 386 | maybe, 387 | maybeNever, 388 | maybeComplete, 389 | single, 390 | singleNever, 391 | completable, 392 | completableNever); 393 | } 394 | 395 | final TestParamType type; 396 | final Flowable flowable; 397 | final Flowable flowableNever; 398 | final Flowable flowableComplete; 399 | final Observable observable; 400 | final Observable observableNever; 401 | final Observable observableComplete; 402 | final Maybe maybe; 403 | final Maybe maybeNever; 404 | final Maybe maybeComplete; 405 | final Single single; 406 | final Single singleNever; 407 | final Completable completable; 408 | final Completable completableNever; 409 | 410 | TestParams( 411 | TestParamType type, 412 | Flowable flowable, 413 | Flowable flowableNever, 414 | Flowable flowableComplete, 415 | Observable observable, 416 | Observable observableNever, 417 | Observable observableComplete, 418 | Maybe maybe, 419 | Maybe maybeNever, 420 | Maybe maybeComplete, 421 | Single single, 422 | Single singleNever, 423 | Completable completable, 424 | Completable completableNever) { 425 | this.type = type; 426 | this.flowable = flowable; 427 | this.flowableNever = flowableNever; 428 | this.flowableComplete = flowableComplete; 429 | this.observable = observable; 430 | this.observableNever = observableNever; 431 | this.observableComplete = observableComplete; 432 | this.maybe = maybe; 433 | this.maybeNever = maybeNever; 434 | this.maybeComplete = maybeComplete; 435 | this.single = single; 436 | this.singleNever = singleNever; 437 | this.completable = completable; 438 | this.completableNever = completableNever; 439 | } 440 | 441 | @Override 442 | public String toString() { 443 | return type.name(); 444 | } 445 | } 446 | } 447 | --------------------------------------------------------------------------------