From ec5e1f097b8a3dc5a53f82eb0ec8f8e14163e58e Mon Sep 17 00:00:00 2001 From: alexklints Date: Fri, 1 Nov 2024 18:13:27 +0300 Subject: [PATCH] added API for sending ANR manually commit_hash:d0528c40499d287e3c230b24282c6e1c177aeef9 --- .mapping.json | 2 + analytics/api/prodRelease.api | 2 + .../io/appmetrica/analytics/AppMetrica.java | 16 +++++ .../io/appmetrica/analytics/IReporter.java | 8 +++ .../analytics/impl/BaseReporter.java | 6 ++ .../jvm/client/AnrFromApiReportingTask.kt | 28 ++++++++ .../impl/crash/jvm/client/ThreadState.java | 3 +- .../impl/crash/utils/ThreadsStateDumper.java | 69 ++++++++++-------- .../analytics/impl/proxy/AppMetricaProxy.java | 12 ++++ .../impl/proxy/ReporterExtendedProxy.java | 13 ++++ .../ReporterSynchronousStageExecutor.kt | 2 + .../synchronous/SynchronousStageExecutor.kt | 2 + .../impl/proxy/validation/Barrier.kt | 8 +++ .../impl/proxy/validation/ReporterBarrier.kt | 8 +++ .../selfreporting/SelfReporterWrapper.java | 10 +++ .../impl/stub/ReporterExtendedStub.java | 5 ++ .../appmetrica/analytics/AppMetricaTests.kt | 8 +++ .../analytics/impl/BaseReporterTest.java | 15 ++++ .../impl/GlobalServiceLocatorGettersTest.java | 4 ++ .../jvm/client/AnrFromApiReportingTaskTest.kt | 70 +++++++++++++++++++ .../crash/utils/ThreadsStateDumperTest.java | 2 +- .../impl/proxy/AppMetricaProxyTest.java | 22 ++++++ .../impl/proxy/ReporterExtendedProxyTest.java | 21 ++++++ .../impl/proxy/validation/BarrierTest.kt | 10 +++ .../proxy/validation/ReporterBarrierTest.kt | 10 +++ .../SelfReporterWrapperTest.java | 17 +++++ .../impl/stub/ReporterExtendedStubTest.java | 7 ++ 27 files changed, 347 insertions(+), 33 deletions(-) create mode 100644 analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTask.kt create mode 100644 analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTaskTest.kt diff --git a/.mapping.json b/.mapping.json index 5b2fc531..e0eb7f37 100644 --- a/.mapping.json +++ b/.mapping.json @@ -533,6 +533,7 @@ "analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/ANRMonitor.java":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/ANRMonitor.java", "analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AllThreads.java":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AllThreads.java", "analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/Anr.java":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/Anr.java", + "analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTask.kt":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTask.kt", "analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrReporter.kt":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrReporter.kt", "analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AppMetricaUncaughtExceptionHandler.kt":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AppMetricaUncaughtExceptionHandler.kt", "analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/CrashProcessor.java":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/CrashProcessor.java", @@ -1496,6 +1497,7 @@ "analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/JvmCrashTest.java":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/JvmCrashTest.java", "analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/ANRMonitorTest.kt":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/ANRMonitorTest.kt", "analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AllThreadsTest.java":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AllThreadsTest.java", + "analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTaskTest.kt":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTaskTest.kt", "analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrTest.java":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrTest.java", "analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AppMetricaUncaughtExceptionHandlerTest.kt":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AppMetricaUncaughtExceptionHandlerTest.kt", "analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/CrashProcessorCompositeTest.kt":"mobile/metrika/android/appmetrica/appmetrica-sdk/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/CrashProcessorCompositeTest.kt", diff --git a/analytics/api/prodRelease.api b/analytics/api/prodRelease.api index 578235de..53a02f85 100644 --- a/analytics/api/prodRelease.api +++ b/analytics/api/prodRelease.api @@ -78,6 +78,7 @@ public final class io.appmetrica.analytics.AppMetrica { public static void putErrorEnvironmentValue([NonNull] String, [Nullable] String) public static void registerAnrListener([NonNull] io.appmetrica.analytics.AnrListener) public static void reportAdRevenue([NonNull] io.appmetrica.analytics.AdRevenue) + public static void reportAnr([NonNull] java.util.Map) public static void reportAppOpen([NonNull] android.app.Activity) public static void reportAppOpen([NonNull] android.content.Intent) public static void reportAppOpen([NonNull] String) @@ -270,6 +271,7 @@ public interface io.appmetrica.analytics.IReporter { public abstract void pauseSession() public abstract void putAppEnvironmentValue([NonNull] String, [Nullable] String) public abstract void reportAdRevenue([NonNull] io.appmetrica.analytics.AdRevenue) + public abstract void reportAnr([NonNull] java.util.Map) public abstract void reportECommerce([NonNull] io.appmetrica.analytics.ecommerce.ECommerceEvent) public abstract void reportError([NonNull] String, [Nullable] String) public abstract void reportError([NonNull] String, [Nullable] String, [Nullable] Throwable) diff --git a/analytics/src/main/java/io/appmetrica/analytics/AppMetrica.java b/analytics/src/main/java/io/appmetrica/analytics/AppMetrica.java index 9ca2733f..8d08a947 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/AppMetrica.java +++ b/analytics/src/main/java/io/appmetrica/analytics/AppMetrica.java @@ -622,4 +622,20 @@ public static void reportExternalAttribution(@NonNull ExternalAttribution value) public static void reportExternalAdRevenue(@NonNull Object... values) { AppMetricaProxyProvider.getProxy().reportExternalAdRevenue(values); } + + /** + * Sends an ANR event (the application is not responding) manually. + * It is an alternative for automatic ANR tracking, which is enabled in the ${@link AppMetricaConfig} configuration + * during SDK activation. + * + * @param allThreads A snapshot of all streams with stack traces. + * Can be received by calling {@link Thread#getAllStackTraces()}. + * + * @see AppMetricaConfig.Builder#withAnrMonitoring(boolean) + * @see AppMetricaConfig.Builder#withAnrMonitoringTimeout(int) + * @see AppMetrica#activate(Context, AppMetricaConfig) + */ + public static void reportAnr(@NonNull Map allThreads) { + AppMetricaProxyProvider.getProxy().reportAnr(allThreads); + } } diff --git a/analytics/src/main/java/io/appmetrica/analytics/IReporter.java b/analytics/src/main/java/io/appmetrica/analytics/IReporter.java index 0ca38360..cf8bd5cb 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/IReporter.java +++ b/analytics/src/main/java/io/appmetrica/analytics/IReporter.java @@ -265,4 +265,12 @@ public interface IReporter { *

If called before metrica initialization, app environment will be cleared right after init */ void clearAppEnvironment(); + + /** + * Sends an ANR event (the application is not responding) manually. + * + * @param allThreads A snapshot of all streams with stack traces. + * Can be received by calling {@link Thread#getAllStackTraces()}. + */ + void reportAnr(@NonNull Map allThreads); } diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/BaseReporter.java b/analytics/src/main/java/io/appmetrica/analytics/impl/BaseReporter.java index ea44b6db..1d68ed88 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/BaseReporter.java +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/BaseReporter.java @@ -14,6 +14,7 @@ import io.appmetrica.analytics.impl.crash.PluginErrorDetailsConverter; import io.appmetrica.analytics.impl.crash.jvm.client.AllThreads; import io.appmetrica.analytics.impl.crash.jvm.client.Anr; +import io.appmetrica.analytics.impl.crash.jvm.client.AnrFromApiReportingTask; import io.appmetrica.analytics.impl.crash.jvm.client.CustomError; import io.appmetrica.analytics.impl.crash.jvm.client.RegularError; import io.appmetrica.analytics.impl.crash.jvm.client.UnhandledException; @@ -517,6 +518,11 @@ public void setDataSendingEnabled(boolean value) { mPublicLogger.info("Updated data sending enabled: %s", value); } + @Override + public void reportAnr(@NonNull Map allThreads) { + new AnrFromApiReportingTask(this, allThreads).execute(); + } + @Override public void reportAnr(@NonNull AllThreads allThreads) { Anr anr = new Anr(allThreads, mExtraMetaInfoRetriever.getBuildId(), mExtraMetaInfoRetriever.isOffline()); diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTask.kt b/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTask.kt new file mode 100644 index 00000000..a591f967 --- /dev/null +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTask.kt @@ -0,0 +1,28 @@ +package io.appmetrica.analytics.impl.crash.jvm.client + +import android.os.Looper +import io.appmetrica.analytics.impl.ClientServiceLocator +import io.appmetrica.analytics.impl.crash.utils.FullStateConverter +import io.appmetrica.analytics.impl.crash.utils.ThreadsStateDumper + +class AnrFromApiReportingTask( + private val anrReporter: AnrReporter, + private val allThreads: Map> +) { + + private val mainThread: Thread = Looper.getMainLooper().thread + + fun execute() { + val threadsStateDumper = ThreadsStateDumper( + object : ThreadsStateDumper.ThreadProvider { + override fun getMainThread(): Thread = this@AnrFromApiReportingTask.mainThread + override fun getMainThreadStacktrace(): Array? = allThreads[mainThread] + override fun getAllThreadsStacktraces(): Map> = allThreads + }, + FullStateConverter(), + ClientServiceLocator.getInstance().processDetector + ) + + anrReporter.reportAnr(threadsStateDumper.threadsDumpForAnr) + } +} diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/ThreadState.java b/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/ThreadState.java index 5490e2c0..8a6ecef1 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/ThreadState.java +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/crash/jvm/client/ThreadState.java @@ -20,7 +20,8 @@ public class ThreadState { public final List stacktrace; public ThreadState(@NonNull String name, - int priority, long tid, + int priority, + long tid, @NonNull String group, @Nullable Integer state, @Nullable List stacktrace) { diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/crash/utils/ThreadsStateDumper.java b/analytics/src/main/java/io/appmetrica/analytics/impl/crash/utils/ThreadsStateDumper.java index 3ba48004..3ce79933 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/crash/utils/ThreadsStateDumper.java +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/crash/utils/ThreadsStateDumper.java @@ -3,7 +3,6 @@ import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import io.appmetrica.analytics.coreapi.internal.backport.BiFunction; import io.appmetrica.analytics.coreutils.internal.StringUtils; import io.appmetrica.analytics.impl.ClientServiceLocator; @@ -18,11 +17,16 @@ public class ThreadsStateDumper { - interface ThreadProvider { + public interface ThreadProvider { + @NonNull Thread getMainThread(); - Map getAllOtherThreads(); + @Nullable + StackTraceElement[] getMainThreadStacktrace(); + + @NonNull + Map getAllThreadsStacktraces(); } @@ -32,24 +36,34 @@ interface ThreadProvider { public ThreadsStateDumper() { this(new ThreadProvider() { - @Override - public Thread getMainThread() { - return Looper.getMainLooper().getThread(); - } - @Override - public Map getAllOtherThreads() { - return Thread.getAllStackTraces(); - } - }, new FullStateConverter(), + @Override + @NonNull + public Thread getMainThread() { + return Looper.getMainLooper().getThread(); + } + + @Nullable + @Override + public StackTraceElement[] getMainThreadStacktrace() { + return null; + } + + @Override + @NonNull + public Map getAllThreadsStacktraces() { + return Thread.getAllStackTraces(); + } + }, new FullStateConverter(), ClientServiceLocator.getInstance().getProcessDetector() ); } - @VisibleForTesting() - ThreadsStateDumper(@NonNull ThreadProvider threadProvider, - @NonNull BiFunction converter, - @NonNull ProcessDetector processDetector) { + public ThreadsStateDumper( + @NonNull ThreadProvider threadProvider, + @NonNull BiFunction converter, + @NonNull ProcessDetector processDetector + ) { this.threadProvider = threadProvider; this.threadConverter = converter; this.processDetector = processDetector; @@ -58,13 +72,12 @@ public Map getAllOtherThreads() { /** * @return String dump of all a thread details and stacktraces. */ - //region changed code public AllThreads getThreadsDumpForAnr() { Thread mainThread = threadProvider.getMainThread(); return new AllThreads( - getMainThreadState(mainThread), - getAllThreadsDump(mainThread, null), - processDetector.getProcessName() + getMainThreadState(mainThread), + getAllThreadsDump(mainThread, null), + processDetector.getProcessName() ); } @@ -80,7 +93,10 @@ public List getThreadsDumpForCrash(@Nullable Thread excludedThread) private ThreadState getMainThreadState(@NonNull Thread mainThread) { StackTraceElement[] mainStackTrace = null; try { - mainStackTrace = mainThread.getStackTrace(); + mainStackTrace = threadProvider.getMainThreadStacktrace(); + if (mainStackTrace == null) { + mainStackTrace = mainThread.getStackTrace(); + } } catch (SecurityException e) { /* do nothing */ } return threadConverter.apply(mainThread, mainStackTrace); @@ -96,9 +112,7 @@ public int compare(Thread first, Thread second) { if (first == second) { return 0; } - //region changed code return StringUtils.compare(first.getName(), second.getName()); - //endregion } }; @@ -108,18 +122,16 @@ public int compare(Thread first, Thread second) { Map allStackTraces = null; try { - allStackTraces = threadProvider.getAllOtherThreads(); + allStackTraces = threadProvider.getAllThreadsStacktraces(); } catch (SecurityException e) { /* do nothing */ } if (allStackTraces != null) { stackTraces.putAll(allStackTraces); } - //region changed code if (excludedThread != null) { stackTraces.remove(excludedThread); } - //endregion for (Map.Entry entry : stackTraces.entrySet()) { final Thread thread = entry.getKey(); @@ -129,14 +141,9 @@ public int compare(Thread first, Thread second) { final StackTraceElement[] stackTrace = entry.getValue(); - //region changed code threads.add(threadConverter.apply(thread, stackTrace)); - //endregion } - //region changed code return threads; } - //endregion - } diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/AppMetricaProxy.java b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/AppMetricaProxy.java index fa115736..82fb7e64 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/AppMetricaProxy.java +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/AppMetricaProxy.java @@ -540,6 +540,18 @@ public void reportExternalAdRevenue(@NonNull Object... values) { }); } + public void reportAnr(@NonNull Map allThreads) { + barrier.reportAnr(allThreads); + synchronousStageExecutor.reportAnr(allThreads); + List> entries = CollectionUtils.getListFromMap(allThreads); + getExecutor().execute(new Runnable() { + @Override + public void run() { + getMainReporter().reportAnr(CollectionUtils.getMapFromList(entries)); + } + }); + } + @VisibleForTesting Barrier getMainFacadeBarrier() { return barrier; diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/ReporterExtendedProxy.java b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/ReporterExtendedProxy.java index 8b826379..4d042643 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/ReporterExtendedProxy.java +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/ReporterExtendedProxy.java @@ -130,6 +130,19 @@ public void run() { }); } + @Override + public void reportAnr(@NonNull Map allThreads) { + barrier.reportAnr(allThreads); + synchronousStageExecutor.reportAnr(allThreads); + List> entries = CollectionUtils.getListFromMap(allThreads); + mExecutor.execute(new Runnable() { + @Override + public void run() { + getReporter().reportAnr(CollectionUtils.getMapFromList(entries)); + } + }); + } + @Override public void reportAnr(@NonNull final AllThreads allThreads) { barrier.reportAnr(allThreads); diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/synchronous/ReporterSynchronousStageExecutor.kt b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/synchronous/ReporterSynchronousStageExecutor.kt index fed14818..b5ff6f9f 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/synchronous/ReporterSynchronousStageExecutor.kt +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/synchronous/ReporterSynchronousStageExecutor.kt @@ -60,5 +60,7 @@ class ReporterSynchronousStageExecutor { fun reportAnr(allThreads: AllThreads) {} + fun reportAnr(allThread: Map>) {} + fun setSessionExtra(key: String, value: ByteArray?) {} } diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/synchronous/SynchronousStageExecutor.kt b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/synchronous/SynchronousStageExecutor.kt index 6c838f92..0d5be101 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/synchronous/SynchronousStageExecutor.kt +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/synchronous/SynchronousStageExecutor.kt @@ -207,4 +207,6 @@ class SynchronousStageExecutor @VisibleForTesting constructor( fun reportExternalAttribution(value: ExternalAttribution) {} fun reportExternalAdRevenue(vararg values: Any) {} + + fun reportAnr(allThreads: Map>) {} } diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/validation/Barrier.kt b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/validation/Barrier.kt index 8134660b..0e7fb731 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/validation/Barrier.kt +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/validation/Barrier.kt @@ -102,6 +102,10 @@ class Barrier( NonNullValidator("ECommerceEvent") ) + private val anrAllThreadValidator = ThrowIfFailedValidator( + NonNullValidator>>("Anr all threads") + ) + fun enableActivityAutoTracking(application: Application?) { applicationValidator.validate(application) } @@ -300,4 +304,8 @@ class Barrier( fun reportExternalAdRevenue(vararg values: Any) { activationValidator.validate() } + + fun reportAnr(allThread: Map>?) { + anrAllThreadValidator.validate(allThread) + } } diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/validation/ReporterBarrier.kt b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/validation/ReporterBarrier.kt index d53d492d..a4c86ccb 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/validation/ReporterBarrier.kt +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/proxy/validation/ReporterBarrier.kt @@ -42,6 +42,10 @@ class ReporterBarrier { NonNullValidator("ECommerceEvent") ) + private val anrAllThreadValidator = ThrowIfFailedValidator( + NonNullValidator>>("ANR all threads") + ) + fun reportEvent(eventName: String?) { eventNameValidator.validate(eventName) } @@ -120,6 +124,10 @@ class ReporterBarrier { fun sendEventsBuffer() {} + fun reportAnr(allThreads: Map>?) { + anrAllThreadValidator.validate(allThreads) + } + fun reportAnr(allThreads: AllThreads?) {} fun activate(config: ReporterConfig?) {} diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/selfreporting/SelfReporterWrapper.java b/analytics/src/main/java/io/appmetrica/analytics/impl/selfreporting/SelfReporterWrapper.java index 9a88a5da..39e18a0c 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/selfreporting/SelfReporterWrapper.java +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/selfreporting/SelfReporterWrapper.java @@ -49,6 +49,16 @@ public void perform(@NonNull IReporterExtended reporter) { }); } + @Override + public void reportAnr(@NonNull Map allThreads) { + processCommand(new IReporterCommandPerformer() { + @Override + public void perform(@NonNull IReporterExtended reporter) { + reporter.reportAnr(allThreads); + } + }); + } + @Override public void reportAnr(@NonNull final AllThreads allThreads) { processCommand(new IReporterCommandPerformer() { diff --git a/analytics/src/main/java/io/appmetrica/analytics/impl/stub/ReporterExtendedStub.java b/analytics/src/main/java/io/appmetrica/analytics/impl/stub/ReporterExtendedStub.java index d4068828..3672012b 100644 --- a/analytics/src/main/java/io/appmetrica/analytics/impl/stub/ReporterExtendedStub.java +++ b/analytics/src/main/java/io/appmetrica/analytics/impl/stub/ReporterExtendedStub.java @@ -125,6 +125,11 @@ public void reportUnhandledException(@NonNull UnhandledException unhandledExcept //Do nothing } + @Override + public void reportAnr(@NonNull Map allThreads) { + // Do nothing + } + @Override public void reportAnr(@NonNull AllThreads allThreads) { //Do nothing diff --git a/analytics/src/test/java/io/appmetrica/analytics/AppMetricaTests.kt b/analytics/src/test/java/io/appmetrica/analytics/AppMetricaTests.kt index d77f688a..6e8c3686 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/AppMetricaTests.kt +++ b/analytics/src/test/java/io/appmetrica/analytics/AppMetricaTests.kt @@ -327,4 +327,12 @@ internal class AppMetricaTests : CommonTest() { AppMetrica.reportExternalAdRevenue(value) verify(proxy).reportExternalAdRevenue(value) } + + @Test + fun reportAnr() { + val mainThread: Thread = mock() + val allThreads = mapOf(mainThread to arrayOf(mock())) + AppMetrica.reportAnr(allThreads) + verify(proxy).reportAnr(allThreads) + } } diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/BaseReporterTest.java b/analytics/src/test/java/io/appmetrica/analytics/impl/BaseReporterTest.java index 15b4eedf..6a2e303b 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/impl/BaseReporterTest.java +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/BaseReporterTest.java @@ -9,6 +9,7 @@ import io.appmetrica.analytics.impl.crash.PluginErrorDetailsConverter; import io.appmetrica.analytics.impl.crash.jvm.client.AllThreads; import io.appmetrica.analytics.impl.crash.jvm.client.Anr; +import io.appmetrica.analytics.impl.crash.jvm.client.AnrFromApiReportingTask; import io.appmetrica.analytics.impl.crash.jvm.client.CustomError; import io.appmetrica.analytics.impl.crash.jvm.client.RegularError; import io.appmetrica.analytics.impl.crash.jvm.client.ThreadState; @@ -111,6 +112,9 @@ public abstract class BaseReporterTest extends BaseReporterData { @Rule public final MockedConstructionRule adRevenueWrapperConstructor = new MockedConstructionRule<>(AdRevenueWrapper.class); + @Rule + public final MockedConstructionRule anrFromApiReportingTaskMockedConstructionRule = + new MockedConstructionRule<>(AnrFromApiReportingTask.class); @Rule public final GlobalServiceLocatorRule globalServiceLocatorRule = new GlobalServiceLocatorRule(); @@ -193,6 +197,17 @@ public void apply() throws Throwable { .checkAll(); } + @Test + public void reportAnrFromApi() { + //noinspection unchecked + Map allThread = mock(HashMap.class); + mReporter.reportAnr(allThread); + assertThat(anrFromApiReportingTaskMockedConstructionRule.getConstructionMock().constructed()).hasSize(1); + assertThat(anrFromApiReportingTaskMockedConstructionRule.getArgumentInterceptor().flatArguments()) + .containsExactly(mReporter, allThread); + verify(anrFromApiReportingTaskMockedConstructionRule.getConstructionMock().constructed().get(0)).execute(); + } + @Test public void testSendEventBufferShouldDispatchToReportsHandler() { when(EventsManager.reportEntry(eq(InternalEvents.EVENT_TYPE_PURGE_BUFFER), any(PublicLogger.class))).thenReturn(mockedEvent); diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/GlobalServiceLocatorGettersTest.java b/analytics/src/test/java/io/appmetrica/analytics/impl/GlobalServiceLocatorGettersTest.java index a1d5caef..0b306a5a 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/impl/GlobalServiceLocatorGettersTest.java +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/GlobalServiceLocatorGettersTest.java @@ -371,6 +371,10 @@ public WaitForActivationDelayBarrier getService(GlobalServiceLocator globalServi public final MockedConstructionRule startupStateHolderMockedConstructionRule = new MockedConstructionRule<>(StartupStateHolder.class); + @Rule + public final MockedConstructionRule referrerHolderMockedConstructionRule = + new MockedConstructionRule<>(ReferrerHolder.class); + private GlobalServiceLocator mGlobalServiceLocator; @Before diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTaskTest.kt b/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTaskTest.kt new file mode 100644 index 00000000..fef4d5e4 --- /dev/null +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/crash/jvm/client/AnrFromApiReportingTaskTest.kt @@ -0,0 +1,70 @@ +package io.appmetrica.analytics.impl.crash.jvm.client + +import android.os.Looper +import io.appmetrica.analytics.impl.ClientServiceLocator +import io.appmetrica.analytics.impl.crash.utils.FullStateConverter +import io.appmetrica.analytics.impl.crash.utils.ThreadsStateDumper +import io.appmetrica.analytics.testutils.ClientServiceLocatorRule +import io.appmetrica.analytics.testutils.CommonTest +import io.appmetrica.analytics.testutils.constructionRule +import io.appmetrica.analytics.testutils.on +import io.appmetrica.analytics.testutils.staticRule +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class AnrFromApiReportingTaskTest : CommonTest() { + + private val anrReporter: AnrReporter = mock() + private val thread: Thread = mock() + private val stacktrace = arrayOf(mock()) + private val allThreads = mapOf(thread to stacktrace) + private val allThreadsDump: AllThreads = mock() + + private val mainThread: Thread = mock() + private val mainLooper: Looper = mock { + on { thread } doReturn mainThread + } + @get:Rule + val looperMockedStaticRule = staticRule { + on { Looper.getMainLooper() } doReturn mainLooper + } + + @get:Rule + val threadsStateDumperMockedConstructionRule = constructionRule { + on { threadsDumpForAnr } doReturn allThreadsDump + } + + @get:Rule + val fullStateConverterConstructionRule = constructionRule() + + @get:Rule + val clientServiceLocatorRule = ClientServiceLocatorRule() + + private val anrFromApiReportingTask by setUp { AnrFromApiReportingTask(anrReporter, allThreads) } + + @Test + fun execute() { + anrFromApiReportingTask.execute() + verify(anrReporter).reportAnr(allThreadsDump) + } + + @Test + fun `execute check thread state dumper`() { + anrFromApiReportingTask.execute() + + assertThat(threadsStateDumperMockedConstructionRule.constructionMock.constructed()).hasSize(1) + val arguments = threadsStateDumperMockedConstructionRule.argumentInterceptor.flatArguments() + val threadProvider = arguments[0] as ThreadsStateDumper.ThreadProvider + assertThat(threadProvider.mainThread).isEqualTo(mainThread) + assertThat(threadProvider.allThreadsStacktraces).isEqualTo(allThreads) + assertThat(arguments[1]).isEqualTo(fullStateConverterConstructionRule.constructionMock.constructed().first()) + assertThat(arguments[2]).isEqualTo(ClientServiceLocator.getInstance().processDetector) + + assertThat(fullStateConverterConstructionRule.constructionMock.constructed()).hasSize(1) + assertThat(fullStateConverterConstructionRule.argumentInterceptor.flatArguments()).isEmpty() + } +} diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/crash/utils/ThreadsStateDumperTest.java b/analytics/src/test/java/io/appmetrica/analytics/impl/crash/utils/ThreadsStateDumperTest.java index b9e0b0ad..0a427a6e 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/impl/crash/utils/ThreadsStateDumperTest.java +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/crash/utils/ThreadsStateDumperTest.java @@ -58,7 +58,7 @@ public void setUp() { } doReturn(mainThreadState).when(threadConverter).apply(same(mainThread), any(StackTraceElement[].class)); doReturn(mainThread).when(threadProvider).getMainThread(); - doReturn(otherThreads).when(threadProvider).getAllOtherThreads(); + doReturn(otherThreads).when(threadProvider).getAllThreadsStacktraces(); } @Test diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/AppMetricaProxyTest.java b/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/AppMetricaProxyTest.java index e0b55c88..4a17f76a 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/AppMetricaProxyTest.java +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/AppMetricaProxyTest.java @@ -679,6 +679,28 @@ public void reportExternalAdRevenue() { order.verify(moduleAdRevenueProcessor).process(value); } + @Test + public void reportAnr() { + Thread thread = mock(Thread.class); + StackTraceElement[] stackTraceElements = new StackTraceElement[]{mock(StackTraceElement.class)}; + Map allThreads = new HashMap<>(); + allThreads.put(thread, stackTraceElements); + //noinspection unchecked + ArgumentCaptor> allThreadsCaptor = ArgumentCaptor.forClass(Map.class); + + mProxy.reportAnr(allThreads); + + InOrder inOrder = inOrder(mBarrier, mSynchronousStageExecutor, mMainReporter); + inOrder.verify(mBarrier).reportAnr(allThreads); + inOrder.verify(mSynchronousStageExecutor).reportAnr(allThreads); + inOrder.verify(mMainReporter).reportAnr(allThreadsCaptor.capture()); + inOrder.verifyNoMoreInteractions(); + + assertThat(allThreadsCaptor.getValue()) + .isNotSameAs(allThreads) + .containsExactlyEntriesOf(allThreads); + } + private void checkSetDataSendingEnabled(boolean value) { mProxy.setDataSendingEnabled(value); InOrder order = inOrder(mBarrier, mSynchronousStageExecutor, mProvider); diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/ReporterExtendedProxyTest.java b/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/ReporterExtendedProxyTest.java index ad25ffa7..1754b326 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/ReporterExtendedProxyTest.java +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/ReporterExtendedProxyTest.java @@ -107,6 +107,27 @@ public void testReportAnr() { inOrder.verify(mReporter).reportAnr(allThreads); } + @Test + public void reportAnrFromApi() { + Thread thread = mock(Thread.class); + StackTraceElement[] stackTraceElements = new StackTraceElement[]{mock(StackTraceElement.class)}; + Map allThreads = new HashMap<>(); + allThreads.put(thread, stackTraceElements); + //noinspection unchecked + ArgumentCaptor> allThreadsCaptor = ArgumentCaptor.forClass(Map.class); + mReporterExtendedProxy.reportAnr(allThreads); + + InOrder inOrder = Mockito.inOrder(reporterBarrier, mSynchronousStageExecutor, mReporter); + inOrder.verify(reporterBarrier).reportAnr(allThreads); + inOrder.verify(mSynchronousStageExecutor).reportAnr(allThreads); + inOrder.verify(mReporter).reportAnr(allThreadsCaptor.capture()); + inOrder.verifyNoMoreInteractions(); + + assertThat(allThreadsCaptor.getValue()) + .isNotSameAs(allThreads) + .containsExactlyEntriesOf(allThreads); + } + @Test public void testSendEventsBuffer() { mReporterExtendedProxy.sendEventsBuffer(); diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/validation/BarrierTest.kt b/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/validation/BarrierTest.kt index d4e5d102..ac01fa2f 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/validation/BarrierTest.kt +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/validation/BarrierTest.kt @@ -620,4 +620,14 @@ class BarrierTest : CommonTest() { whenever(appMetricaFacadeProvider.isActivated).thenReturn(false) mBarrier.reportExternalAdRevenue("string") } + + @Test + fun `reportAnr for valid values`() { + mBarrier.reportAnr(mapOf(mock() to arrayOf(mock()))) + } + + @Test(expected = ValidationException::class) + fun `reportAnr for null all threads`() { + mBarrier.reportAnr(null) + } } diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/validation/ReporterBarrierTest.kt b/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/validation/ReporterBarrierTest.kt index 3ac0b185..645d0d7a 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/validation/ReporterBarrierTest.kt +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/proxy/validation/ReporterBarrierTest.kt @@ -195,6 +195,16 @@ class ReporterBarrierTest : CommonTest() { mBarrier.reportAnr(mock()) } + @Test + fun `reportAnr from api for valid values`() { + mBarrier.reportAnr(mock>>()) + } + + @Test(expected = ValidationException::class) + fun `reportAnr from api for null all threads`() { + mBarrier.reportAnr(null as Map>?) + } + @Test fun activate() { mBarrier.activate(mock()) diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/selfreporting/SelfReporterWrapperTest.java b/analytics/src/test/java/io/appmetrica/analytics/impl/selfreporting/SelfReporterWrapperTest.java index 1dedffb9..d80c4e52 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/impl/selfreporting/SelfReporterWrapperTest.java +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/selfreporting/SelfReporterWrapperTest.java @@ -114,6 +114,23 @@ public void testReportAnrNotInitialized() { verify(mReporter).reportAnr(allThreads); } + @Test + public void reportAnrFromApiInitialized() { + Map allThread = mock(); + mSelfReporterWrapper.onInitializationFinished(mContext); + mSelfReporterWrapper.reportAnr(allThread); + verify(mReporter).reportAnr(allThread); + } + + @Test + public void reportAnrFromApiNotInitialized() { + Map allThread = mock(); + mSelfReporterWrapper.reportAnr(allThread); + verify(mReporter, never()).reportAnr(allThread); + mSelfReporterWrapper.onInitializationFinished(mContext); + verify(mReporter).reportAnr(allThread); + } + @Test public void testPutAppEnvironmentValueInitialized() { final String key = "aaa"; diff --git a/analytics/src/test/java/io/appmetrica/analytics/impl/stub/ReporterExtendedStubTest.java b/analytics/src/test/java/io/appmetrica/analytics/impl/stub/ReporterExtendedStubTest.java index d01e1663..f46b855d 100644 --- a/analytics/src/test/java/io/appmetrica/analytics/impl/stub/ReporterExtendedStubTest.java +++ b/analytics/src/test/java/io/appmetrica/analytics/impl/stub/ReporterExtendedStubTest.java @@ -16,6 +16,7 @@ import org.robolectric.RobolectricTestRunner; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyNoInteractions; @RunWith(RobolectricTestRunner.class) @@ -137,6 +138,12 @@ public void reportAnr() { verifyNoInteractions(allThreads); } + @Test + public void reportAnrFromApi() { + //noinspection unchecked + getStub().reportAnr(mock(HashMap.class)); + } + @Test public void getPluginExtension() { assertThat(getStub().getPluginExtension()).isInstanceOf(PluginReporterStub.class);