From c7c70adad1d9480ef1d2e74a3e31d4e5b4ebd2c0 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 4 Mar 2022 16:36:47 +0100 Subject: [PATCH 01/20] Add Timber and Fragment integrations if they are present on the classpath --- .../main/java/io/sentry/util}/LoadClass.java | 2 +- sentry-android-core/build.gradle.kts | 4 + .../android/core/ActivityFramesTracker.java | 1 + .../core/AndroidOptionsInitializer.java | 54 ++++++++- .../core/UserInteractionIntegration.java | 1 + .../android/core/ActivityFramesTrackerTest.kt | 1 + .../core/AndroidOptionsInitializerTest.kt | 113 +++++++++++++++++- .../core/UserInteractionIntegrationTest.kt | 1 + .../fragment/FragmentLifecycleIntegration.kt | 2 +- .../sentry/samples/android/MyApplication.java | 11 +- 10 files changed, 178 insertions(+), 12 deletions(-) rename {sentry-android-core/src/main/java/io/sentry/android/core => Sentry/src/main/java/io/sentry/util}/LoadClass.java (93%) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java b/Sentry/src/main/java/io/sentry/util/LoadClass.java similarity index 93% rename from sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java rename to Sentry/src/main/java/io/sentry/util/LoadClass.java index 9e1db9b0e1..6896647c63 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java +++ b/Sentry/src/main/java/io/sentry/util/LoadClass.java @@ -1,4 +1,4 @@ -package io.sentry.android.core; +package io.sentry.util; import org.jetbrains.annotations.NotNull; diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index c92c80743a..365da8ae9a 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -78,6 +78,8 @@ tasks.withType().configureEach { dependencies { api(projects.sentry) + compileOnly(projects.sentryAndroidFragment) + compileOnly(projects.sentryAndroidTimber) // lifecycle processor, session tracking implementation(Config.Libs.lifecycleProcess) @@ -102,4 +104,6 @@ dependencies { testImplementation(Config.TestLibs.mockitoInline) testImplementation(Config.TestLibs.awaitility) testImplementation(projects.sentryTestSupport) + testImplementation(projects.sentryAndroidFragment) + testImplementation(projects.sentryAndroidTimber) } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java index fda5b25e6a..0c72179650 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java @@ -5,6 +5,7 @@ import androidx.core.app.FrameMetricsAggregator; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; +import io.sentry.util.LoadClass; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index d69c8890c2..e238870c05 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -1,7 +1,5 @@ package io.sentry.android.core; -import static io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME; - import android.app.Application; import android.content.Context; import android.content.pm.PackageInfo; @@ -13,6 +11,9 @@ import io.sentry.SendFireAndForgetOutboxSender; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.android.fragment.FragmentLifecycleIntegration; +import io.sentry.android.timber.SentryTimberIntegration; +import io.sentry.util.LoadClass; import io.sentry.util.Objects; import java.io.BufferedInputStream; import java.io.File; @@ -23,6 +24,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME; + /** * Android Options initializer, it reads configurations from AndroidManifest and sets to the * SentryOptions. It also adds default values for some fields. @@ -30,6 +33,12 @@ @SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references final class AndroidOptionsInitializer { + static final String SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME = + "io.sentry.android.fragment.FragmentLifecycleIntegration"; + + static final String SENTRY_TIMBER_INTEGRATION_CLASS_NAME = + "io.sentry.android.timber.SentryTimberIntegration"; + /** private ctor */ private AndroidOptionsInitializer() {} @@ -155,12 +164,31 @@ private static void installDefaultIntegrations( new ActivityLifecycleIntegration( (Application) context, buildInfoProvider, activityFramesTracker)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); + if (isIntegrationAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options, loadClass)) { + options.addIntegration( + new FragmentLifecycleIntegration((Application) context, true, true)); + } else { + options + .getLogger() + .log( + SentryLevel.WARNING, + "sentry-android-fragment is not available, FragmentLifecycleIntegration won't be installed"); + } } else { options .getLogger() .log( SentryLevel.WARNING, - "ActivityLifecycle and UserInteraction Integrations need an Application class to be installed."); + "ActivityLifecycle, FragmentLifecycle and UserInteraction Integrations need an Application class to be installed."); + } + if (isIntegrationAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options, loadClass)) { + options.addIntegration(new SentryTimberIntegration()); + } else { + options + .getLogger() + .log( + SentryLevel.WARNING, + "sentry-android-timber is not available, SentryTimberIntegration won't be installed"); } options.addIntegration(new AppComponentsBreadcrumbsIntegration(context)); options.addIntegration(new SystemEventsBreadcrumbsIntegration(context)); @@ -277,4 +305,24 @@ private static boolean isNdkAvailable(final @NotNull IBuildInfoProvider buildInf } return null; } + + private static boolean isIntegrationAvailable( + final @NotNull String integrationClassName, + final @NotNull SentryOptions options, + final @NotNull LoadClass loadClass + ) { + boolean isAvailable; + try { + loadClass.loadClass(integrationClassName); + isAvailable = true; + } catch (ClassNotFoundException ignored) { + isAvailable = false; + options.getLogger() + .log( + SentryLevel.INFO, + integrationClassName + "won't be installed as it's not available on the classpath" + ); + } + return isAvailable; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java index 7a851b74bd..4ac977e229 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java @@ -12,6 +12,7 @@ import io.sentry.android.core.internal.gestures.NoOpWindowCallback; import io.sentry.android.core.internal.gestures.SentryGestureListener; import io.sentry.android.core.internal.gestures.SentryWindowCallback; +import io.sentry.util.LoadClass; import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt index a7cc4e1d20..2ad82433f5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt @@ -8,6 +8,7 @@ import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever import io.sentry.protocol.SentryId +import io.sentry.util.LoadClass import org.junit.runner.RunWith import kotlin.test.Test import kotlin.test.assertEquals diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index f0a0de175e..33a3c09739 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -6,9 +6,12 @@ import android.os.Bundle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.atLeastOnce import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import io.sentry.ILogger @@ -17,9 +20,11 @@ import io.sentry.SendCachedEnvelopeFireAndForgetIntegration import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME +import io.sentry.android.fragment.FragmentLifecycleIntegration +import io.sentry.android.timber.SentryTimberIntegration +import io.sentry.util.LoadClass import org.junit.runner.RunWith import java.io.File -import java.lang.RuntimeException import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -336,6 +341,106 @@ class AndroidOptionsInitializerTest { assertNull(actual) } + @Test + fun `FragmentLifecycleIntegration added to the integration list if available on classpath`() { + val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) + val logger = mock() + val sentryOptions = SentryAndroidOptions().apply { + setDebug(true) + } + + AndroidOptionsInitializer.init( + sentryOptions, + mockContext, + logger, + createBuildInfo(), + createClassMock(FragmentLifecycleIntegration::class.java) + ) + + val actual = sentryOptions.integrations.firstOrNull { it is FragmentLifecycleIntegration } + assertNotNull(actual) + + verify(logger, never()).log(eq(SentryLevel.WARNING), any(), any()) + } + + @Test + fun `FragmentLifecycleIntegration won't be enabled, it throws class not found`() { + val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) + val logger = mock() + val sentryOptions = SentryAndroidOptions().apply { + setDebug(true) + } + + AndroidOptionsInitializer.init( + sentryOptions, + mockContext, + logger, + createBuildInfo(), + createClassMockThrows( + ClassNotFoundException(), + AndroidOptionsInitializer.SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME + ) + ) + + val actual = sentryOptions.integrations.firstOrNull { it is FragmentLifecycleIntegration } + assertNull(actual) + + verify(logger).log( + eq(SentryLevel.WARNING), + eq("sentry-android-fragment is not available, FragmentLifecycleIntegration won't be installed") + ) + } + + @Test + fun `SentryTimberIntegration added to the integration list if available on classpath`() { + val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) + val logger = mock() + val sentryOptions = SentryAndroidOptions().apply { + setDebug(true) + } + + AndroidOptionsInitializer.init( + sentryOptions, + mockContext, + logger, + createBuildInfo(), + createClassMock(SentryTimberIntegration::class.java) + ) + + val actual = sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } + assertNotNull(actual) + + verify(logger, never()).log(eq(SentryLevel.WARNING), any(), any()) + } + + @Test + fun `SentryTimberIntegration won't be enabled, it throws class not found`() { + val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) + val logger = mock() + val sentryOptions = SentryAndroidOptions().apply { + setDebug(true) + } + + AndroidOptionsInitializer.init( + sentryOptions, + mockContext, + logger, + createBuildInfo(), + createClassMockThrows( + ClassNotFoundException(), + AndroidOptionsInitializer.SENTRY_TIMBER_INTEGRATION_CLASS_NAME + ) + ) + + val actual = sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } + assertNull(actual) + + verify(logger).log( + eq(SentryLevel.WARNING), + eq("sentry-android-timber is not available, SentryTimberIntegration won't be installed") + ) + } + private fun createMockContext(): Context { val mockContext = ContextUtilsTest.createMockContext() whenever(mockContext.cacheDir).thenReturn(file) @@ -366,9 +471,11 @@ class AndroidOptionsInitializerTest { return loadClassMock } - private fun createClassMockThrows(ex: Throwable): LoadClass { + private fun createClassMockThrows( + ex: Throwable, className: String = SENTRY_NDK_CLASS_NAME + ): LoadClass { val loadClassMock = mock() - whenever(loadClassMock.loadClass(eq(SENTRY_NDK_CLASS_NAME))).thenThrow(ex) + whenever(loadClassMock.loadClass(eq(className))).thenThrow(ex) return loadClassMock } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt index 1f4e993695..360a51e9f4 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt @@ -16,6 +16,7 @@ import com.nhaarman.mockitokotlin2.whenever import io.sentry.Hub import io.sentry.android.core.internal.gestures.NoOpWindowCallback import io.sentry.android.core.internal.gestures.SentryWindowCallback +import io.sentry.util.LoadClass import org.junit.Test import org.junit.runner.RunWith import kotlin.test.assertTrue diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt index dfa2b145cb..54f2edca47 100644 --- a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt @@ -20,7 +20,7 @@ class FragmentLifecycleIntegration( Integration, Closeable { - constructor(application: Application) : this(application, true, false) + constructor(application: Application) : this(application, true, true) private lateinit var hub: IHub private lateinit var options: SentryOptions diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index 301435206b..7471892ab2 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -3,8 +3,6 @@ import android.app.Application; import android.os.StrictMode; import io.sentry.android.core.SentryAndroid; -import io.sentry.android.fragment.FragmentLifecycleIntegration; -import io.sentry.android.timber.SentryTimberIntegration; /** Apps. main Application. */ public class MyApplication extends Application { @@ -19,8 +17,13 @@ public void onCreate() { SentryAndroid.init( this, options -> { - options.addIntegration(new FragmentLifecycleIntegration(MyApplication.this, true, true)); - options.addIntegration(new SentryTimberIntegration()); + /* + use options, for example, to add a beforeSend callback: + + options.setBeforeSend((event, hint) -> { + process event + }); + */ }); } From 12faa15c11d28e4deb3899fd2d53e17618e82403 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 4 Mar 2022 16:37:34 +0100 Subject: [PATCH 02/20] spotless --- .../core/AndroidOptionsInitializer.java | 44 +++++++++---------- .../core/AndroidOptionsInitializerTest.kt | 5 +-- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index e238870c05..c6bee552dc 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME; + import android.app.Application; import android.content.Context; import android.content.pm.PackageInfo; @@ -24,8 +26,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import static io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME; - /** * Android Options initializer, it reads configurations from AndroidManifest and sets to the * SentryOptions. It also adds default values for some fields. @@ -34,10 +34,10 @@ final class AndroidOptionsInitializer { static final String SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME = - "io.sentry.android.fragment.FragmentLifecycleIntegration"; + "io.sentry.android.fragment.FragmentLifecycleIntegration"; static final String SENTRY_TIMBER_INTEGRATION_CLASS_NAME = - "io.sentry.android.timber.SentryTimberIntegration"; + "io.sentry.android.timber.SentryTimberIntegration"; /** private ctor */ private AndroidOptionsInitializer() {} @@ -165,14 +165,13 @@ private static void installDefaultIntegrations( (Application) context, buildInfoProvider, activityFramesTracker)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); if (isIntegrationAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options, loadClass)) { - options.addIntegration( - new FragmentLifecycleIntegration((Application) context, true, true)); + options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true)); } else { options - .getLogger() - .log( - SentryLevel.WARNING, - "sentry-android-fragment is not available, FragmentLifecycleIntegration won't be installed"); + .getLogger() + .log( + SentryLevel.WARNING, + "sentry-android-fragment is not available, FragmentLifecycleIntegration won't be installed"); } } else { options @@ -185,10 +184,10 @@ private static void installDefaultIntegrations( options.addIntegration(new SentryTimberIntegration()); } else { options - .getLogger() - .log( - SentryLevel.WARNING, - "sentry-android-timber is not available, SentryTimberIntegration won't be installed"); + .getLogger() + .log( + SentryLevel.WARNING, + "sentry-android-timber is not available, SentryTimberIntegration won't be installed"); } options.addIntegration(new AppComponentsBreadcrumbsIntegration(context)); options.addIntegration(new SystemEventsBreadcrumbsIntegration(context)); @@ -307,21 +306,20 @@ private static boolean isNdkAvailable(final @NotNull IBuildInfoProvider buildInf } private static boolean isIntegrationAvailable( - final @NotNull String integrationClassName, - final @NotNull SentryOptions options, - final @NotNull LoadClass loadClass - ) { + final @NotNull String integrationClassName, + final @NotNull SentryOptions options, + final @NotNull LoadClass loadClass) { boolean isAvailable; try { loadClass.loadClass(integrationClassName); isAvailable = true; } catch (ClassNotFoundException ignored) { isAvailable = false; - options.getLogger() - .log( - SentryLevel.INFO, - integrationClassName + "won't be installed as it's not available on the classpath" - ); + options + .getLogger() + .log( + SentryLevel.INFO, + integrationClassName + "won't be installed as it's not available on the classpath"); } return isAvailable; } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 33a3c09739..b6ae5f4738 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -6,8 +6,6 @@ import android.os.Bundle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.atLeastOnce import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never @@ -472,7 +470,8 @@ class AndroidOptionsInitializerTest { } private fun createClassMockThrows( - ex: Throwable, className: String = SENTRY_NDK_CLASS_NAME + ex: Throwable, + className: String = SENTRY_NDK_CLASS_NAME ): LoadClass { val loadClassMock = mock() whenever(loadClassMock.loadClass(eq(className))).thenThrow(ex) From 1cfe6fc7d0d3e7508b1687cc26834ea75471b8a3 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 4 Mar 2022 16:39:53 +0100 Subject: [PATCH 03/20] api --- sentry-android-core/api/sentry-android-core.api | 9 ++------- sentry/api/sentry.api | 5 +++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 689c85e35a..1b2e9ba442 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -1,5 +1,5 @@ public final class io/sentry/android/core/ActivityFramesTracker { - public fun (Lio/sentry/android/core/LoadClass;)V + public fun (Lio/sentry/util/LoadClass;)V public fun addActivity (Landroid/app/Activity;)V public fun setMetrics (Landroid/app/Activity;Lio/sentry/protocol/SentryId;)V public fun stop ()V @@ -80,11 +80,6 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader { public abstract fun loadDebugImages ()Ljava/util/List; } -public final class io/sentry/android/core/LoadClass { - public fun ()V - public fun loadClass (Ljava/lang/String;)Ljava/lang/Class; -} - public final class io/sentry/android/core/NdkIntegration : io/sentry/Integration, java/io/Closeable { public static final field SENTRY_NDK_CLASS_NAME Ljava/lang/String; public fun (Ljava/lang/Class;)V @@ -178,7 +173,7 @@ public final class io/sentry/android/core/TempSensorBreadcrumbsIntegration : and } public final class io/sentry/android/core/UserInteractionIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable { - public fun (Landroid/app/Application;Lio/sentry/android/core/LoadClass;)V + public fun (Landroid/app/Application;Lio/sentry/util/LoadClass;)V public fun close ()V public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V public fun onActivityDestroyed (Landroid/app/Activity;)V diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index ad771d4e54..4983118dcb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2046,6 +2046,11 @@ public final class io/sentry/util/ExceptionUtils { public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable; } +public final class io/sentry/util/LoadClass { + public fun ()V + public fun loadClass (Ljava/lang/String;)Ljava/lang/Class; +} + public final class io/sentry/util/LogUtils { public fun ()V public static fun logIfNotFlushable (Lio/sentry/ILogger;Ljava/lang/Object;)V From 9f30d386027217185505932cff593891efc2838a Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 4 Mar 2022 16:58:32 +0100 Subject: [PATCH 04/20] Simplify log logic --- .../android/core/AndroidOptionsInitializer.java | 16 ++-------------- .../core/AndroidOptionsInitializerTest.kt | 4 ++-- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index c6bee552dc..4fae123689 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -166,12 +166,6 @@ private static void installDefaultIntegrations( options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); if (isIntegrationAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options, loadClass)) { options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true)); - } else { - options - .getLogger() - .log( - SentryLevel.WARNING, - "sentry-android-fragment is not available, FragmentLifecycleIntegration won't be installed"); } } else { options @@ -182,12 +176,6 @@ private static void installDefaultIntegrations( } if (isIntegrationAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options, loadClass)) { options.addIntegration(new SentryTimberIntegration()); - } else { - options - .getLogger() - .log( - SentryLevel.WARNING, - "sentry-android-timber is not available, SentryTimberIntegration won't be installed"); } options.addIntegration(new AppComponentsBreadcrumbsIntegration(context)); options.addIntegration(new SystemEventsBreadcrumbsIntegration(context)); @@ -318,8 +306,8 @@ private static boolean isIntegrationAvailable( options .getLogger() .log( - SentryLevel.INFO, - integrationClassName + "won't be installed as it's not available on the classpath"); + SentryLevel.WARNING, + integrationClassName + " won't be installed as it's not available on the classpath"); } return isAvailable; } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index b6ae5f4738..8c112fddab 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -385,7 +385,7 @@ class AndroidOptionsInitializerTest { verify(logger).log( eq(SentryLevel.WARNING), - eq("sentry-android-fragment is not available, FragmentLifecycleIntegration won't be installed") + eq("${AndroidOptionsInitializer.SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME} won't be installed as it's not available on the classpath") ) } @@ -435,7 +435,7 @@ class AndroidOptionsInitializerTest { verify(logger).log( eq(SentryLevel.WARNING), - eq("sentry-android-timber is not available, SentryTimberIntegration won't be installed") + eq("${AndroidOptionsInitializer.SENTRY_TIMBER_INTEGRATION_CLASS_NAME} won't be installed as it's not available on the classpath") ) } From 44e82b07f81ba1e415db546c4bd16c93c4340351 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Sun, 6 Mar 2022 21:11:17 +0100 Subject: [PATCH 05/20] Changelog --- CHANGELOG.md | 5 +++++ .../io/sentry/android/core/AndroidOptionsInitializer.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 433cbb2645..69151be51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +* Feat: Automatically enable `Timber` and `Fragment` integrations if they are present on the classpath (#1936) + +Behaviour change: +[Performance tracking](https://docs.sentry.io/platforms/android/configuration/integrations/fragment/#configure) for fragment's lifecycle is enabled by default now. + ## 5.6.2 ### Various fixes & improvements diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 4fae123689..6fb32ad100 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -165,7 +165,7 @@ private static void installDefaultIntegrations( (Application) context, buildInfoProvider, activityFramesTracker)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); if (isIntegrationAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options, loadClass)) { - options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true)); + options.addIntegration(new FragmentLifecycleIntegration((Application) context)); } } else { options From 3ec52292e0af6f129c692930a02f46a839219353 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Sun, 6 Mar 2022 21:31:29 +0100 Subject: [PATCH 06/20] Revert LoadClass moving --- sentry-android-core/api/sentry-android-core.api | 9 +++++++-- .../io/sentry/android/core/ActivityFramesTracker.java | 1 - .../sentry/android/core/AndroidOptionsInitializer.java | 1 - .../src/main/java/io/sentry/android/core}/LoadClass.java | 2 +- .../sentry/android/core/UserInteractionIntegration.java | 1 - .../io/sentry/android/core/ActivityFramesTrackerTest.kt | 1 - .../sentry/android/core/AndroidOptionsInitializerTest.kt | 2 -- .../android/core/UserInteractionIntegrationTest.kt | 1 - sentry/api/sentry.api | 5 ----- 9 files changed, 8 insertions(+), 15 deletions(-) rename {Sentry/src/main/java/io/sentry/util => sentry-android-core/src/main/java/io/sentry/android/core}/LoadClass.java (93%) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 1b2e9ba442..689c85e35a 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -1,5 +1,5 @@ public final class io/sentry/android/core/ActivityFramesTracker { - public fun (Lio/sentry/util/LoadClass;)V + public fun (Lio/sentry/android/core/LoadClass;)V public fun addActivity (Landroid/app/Activity;)V public fun setMetrics (Landroid/app/Activity;Lio/sentry/protocol/SentryId;)V public fun stop ()V @@ -80,6 +80,11 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader { public abstract fun loadDebugImages ()Ljava/util/List; } +public final class io/sentry/android/core/LoadClass { + public fun ()V + public fun loadClass (Ljava/lang/String;)Ljava/lang/Class; +} + public final class io/sentry/android/core/NdkIntegration : io/sentry/Integration, java/io/Closeable { public static final field SENTRY_NDK_CLASS_NAME Ljava/lang/String; public fun (Ljava/lang/Class;)V @@ -173,7 +178,7 @@ public final class io/sentry/android/core/TempSensorBreadcrumbsIntegration : and } public final class io/sentry/android/core/UserInteractionIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable { - public fun (Landroid/app/Application;Lio/sentry/util/LoadClass;)V + public fun (Landroid/app/Application;Lio/sentry/android/core/LoadClass;)V public fun close ()V public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V public fun onActivityDestroyed (Landroid/app/Activity;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java index 0c72179650..fda5b25e6a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java @@ -5,7 +5,6 @@ import androidx.core.app.FrameMetricsAggregator; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; -import io.sentry.util.LoadClass; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 6fb32ad100..4eee789dbc 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -15,7 +15,6 @@ import io.sentry.SentryOptions; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; -import io.sentry.util.LoadClass; import io.sentry.util.Objects; import java.io.BufferedInputStream; import java.io.File; diff --git a/Sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java similarity index 93% rename from Sentry/src/main/java/io/sentry/util/LoadClass.java rename to sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java index 6896647c63..9e1db9b0e1 100644 --- a/Sentry/src/main/java/io/sentry/util/LoadClass.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java @@ -1,4 +1,4 @@ -package io.sentry.util; +package io.sentry.android.core; import org.jetbrains.annotations.NotNull; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java index 4ac977e229..7a851b74bd 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java @@ -12,7 +12,6 @@ import io.sentry.android.core.internal.gestures.NoOpWindowCallback; import io.sentry.android.core.internal.gestures.SentryGestureListener; import io.sentry.android.core.internal.gestures.SentryWindowCallback; -import io.sentry.util.LoadClass; import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt index 2ad82433f5..a7cc4e1d20 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt @@ -8,7 +8,6 @@ import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever import io.sentry.protocol.SentryId -import io.sentry.util.LoadClass import org.junit.runner.RunWith import kotlin.test.Test import kotlin.test.assertEquals diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 8c112fddab..30c617de2e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -9,7 +9,6 @@ import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import io.sentry.ILogger @@ -20,7 +19,6 @@ import io.sentry.SentryOptions import io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration -import io.sentry.util.LoadClass import org.junit.runner.RunWith import java.io.File import kotlin.test.BeforeTest diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt index 360a51e9f4..1f4e993695 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt @@ -16,7 +16,6 @@ import com.nhaarman.mockitokotlin2.whenever import io.sentry.Hub import io.sentry.android.core.internal.gestures.NoOpWindowCallback import io.sentry.android.core.internal.gestures.SentryWindowCallback -import io.sentry.util.LoadClass import org.junit.Test import org.junit.runner.RunWith import kotlin.test.assertTrue diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4983118dcb..ad771d4e54 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2046,11 +2046,6 @@ public final class io/sentry/util/ExceptionUtils { public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable; } -public final class io/sentry/util/LoadClass { - public fun ()V - public fun loadClass (Ljava/lang/String;)Ljava/lang/Class; -} - public final class io/sentry/util/LogUtils { public fun ()V public static fun logIfNotFlushable (Lio/sentry/ILogger;Ljava/lang/Object;)V From a5830bf4cf0fe497da8487d04e7b0591671bff8d Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Sun, 6 Mar 2022 21:54:59 +0100 Subject: [PATCH 07/20] Disable Timber lint rule --- sentry-android-core/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 365da8ae9a..7937655aad 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -49,6 +49,7 @@ android { // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks. checkReleaseBuilds = false + disable += "LogNotTimber" } // needed because of Kotlin 1.4.x From 3d17156f3b35b7db849af5b377357fe374397246 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 10 Mar 2022 16:21:47 +0100 Subject: [PATCH 08/20] Refactor LoadClass and AndroidOptionsInitializer --- .../android/core/ActivityFramesTracker.java | 17 +- .../core/AndroidOptionsInitializer.java | 51 +- .../io/sentry/android/core/LoadClass.java | 34 +- .../core/UserInteractionIntegration.java | 24 +- .../android/core/ActivityFramesTrackerTest.kt | 19 +- .../core/AndroidOptionsInitializerTest.kt | 439 ++++++------------ .../sentry/android/core/ContextUtilsTest.kt | 10 +- .../core/UserInteractionIntegrationTest.kt | 10 +- 8 files changed, 217 insertions(+), 387 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java index fda5b25e6a..94b7e80c83 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.util.SparseIntArray; import androidx.core.app.FrameMetricsAggregator; +import io.sentry.SentryOptions; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import java.util.HashMap; @@ -25,8 +26,10 @@ public final class ActivityFramesTracker { private final @NotNull Map> activityMeasurements = new ConcurrentHashMap<>(); - public ActivityFramesTracker(final @NotNull LoadClass loadClass) { - androidXAvailable = checkAndroidXAvailability(loadClass); + public ActivityFramesTracker( + final @NotNull LoadClass loadClass, final @NotNull SentryOptions options) { + androidXAvailable = + loadClass.isClassAvailable("androidx.core.app.FrameMetricsAggregator", options); if (androidXAvailable) { frameMetricsAggregator = new FrameMetricsAggregator(); } @@ -37,16 +40,6 @@ public ActivityFramesTracker(final @NotNull LoadClass loadClass) { this.frameMetricsAggregator = frameMetricsAggregator; } - private static boolean checkAndroidXAvailability(final @NotNull LoadClass loadClass) { - try { - loadClass.loadClass("androidx.core.app.FrameMetricsAggregator"); - return true; - } catch (ClassNotFoundException ignored) { - // androidx.core isn't available. - return false; - } - } - private boolean isFrameMetricsAggregatorAvailable() { return androidXAvailable && frameMetricsAggregator != null; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 4eee789dbc..7de89acabc 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -115,7 +115,8 @@ static void init( ManifestMetadataReader.applyMetadata(context, options); initializeCacheDirs(context, options); - final ActivityFramesTracker activityFramesTracker = new ActivityFramesTracker(loadClass); + final ActivityFramesTracker activityFramesTracker = + new ActivityFramesTracker(loadClass, options); installDefaultIntegrations( context, options, buildInfoProvider, loadClass, activityFramesTracker); @@ -140,7 +141,10 @@ private static void installDefaultIntegrations( // Integrations are registered in the same order. NDK before adding Watch outbox, // because sentry-native move files around and we don't want to watch that. - final Class sentryNdkClass = loadNdkIfAvailable(options, buildInfoProvider, loadClass); + final Class sentryNdkClass = + isNdkAvailable(buildInfoProvider) + ? loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options) + : null; options.addIntegration(new NdkIntegration(sentryNdkClass)); // this integration uses android.os.FileObserver, we can't move to sentry @@ -163,7 +167,7 @@ private static void installDefaultIntegrations( new ActivityLifecycleIntegration( (Application) context, buildInfoProvider, activityFramesTracker)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); - if (isIntegrationAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options, loadClass)) { + if (loadClass.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options)) { options.addIntegration(new FragmentLifecycleIntegration((Application) context)); } } else { @@ -173,7 +177,7 @@ private static void installDefaultIntegrations( SentryLevel.WARNING, "ActivityLifecycle, FragmentLifecycle and UserInteraction Integrations need an Application class to be installed."); } - if (isIntegrationAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options, loadClass)) { + if (loadClass.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options)) { options.addIntegration(new SentryTimberIntegration()); } options.addIntegration(new AppComponentsBreadcrumbsIntegration(context)); @@ -271,43 +275,4 @@ private static void initializeCacheDirs( private static boolean isNdkAvailable(final @NotNull IBuildInfoProvider buildInfoProvider) { return buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN; } - - private static @Nullable Class loadNdkIfAvailable( - final @NotNull SentryOptions options, - final @NotNull IBuildInfoProvider buildInfoProvider, - final @NotNull LoadClass loadClass) { - if (isNdkAvailable(buildInfoProvider)) { - try { - return loadClass.loadClass(SENTRY_NDK_CLASS_NAME); - } catch (ClassNotFoundException e) { - options.getLogger().log(SentryLevel.ERROR, "Failed to load SentryNdk.", e); - } catch (UnsatisfiedLinkError e) { - options - .getLogger() - .log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) SentryNdk.", e); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Failed to initialize SentryNdk.", e); - } - } - return null; - } - - private static boolean isIntegrationAvailable( - final @NotNull String integrationClassName, - final @NotNull SentryOptions options, - final @NotNull LoadClass loadClass) { - boolean isAvailable; - try { - loadClass.loadClass(integrationClassName); - isAvailable = true; - } catch (ClassNotFoundException ignored) { - isAvailable = false; - options - .getLogger() - .log( - SentryLevel.WARNING, - integrationClassName + " won't be installed as it's not available on the classpath"); - } - return isAvailable; - } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java index 9e1db9b0e1..92b5cdfa71 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java @@ -1,6 +1,9 @@ package io.sentry.android.core; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** An Adapter for making Class.forName testable */ public final class LoadClass { @@ -9,10 +12,33 @@ public final class LoadClass { * Try to load a class via reflection * * @param clazz the full class name - * @return a Class - * @throws ClassNotFoundException if class is not found + * @param options an instance of SentryOptions + * @return a Class if it's available, or null */ - public @NotNull Class loadClass(@NotNull String clazz) throws ClassNotFoundException { - return Class.forName(clazz); + public @Nullable Class loadClass( + final @NotNull String clazz, final @Nullable SentryOptions options) { + try { + return Class.forName(clazz); + } catch (ClassNotFoundException e) { + if (options != null) { + options.getLogger().log(SentryLevel.WARNING, "Failed to load class " + clazz, e); + } + } catch (UnsatisfiedLinkError e) { + if (options != null) { + options + .getLogger() + .log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) " + clazz, e); + } + } catch (Throwable e) { + if (options != null) { + options.getLogger().log(SentryLevel.ERROR, "Failed to initialize " + clazz, e); + } + } + return null; + } + + public boolean isClassAvailable( + final @NotNull String clazz, final @Nullable SentryOptions options) { + return loadClass(clazz, options) != null; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java index 7a851b74bd..4f917f86eb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java @@ -33,26 +33,10 @@ public UserInteractionIntegration( final @NotNull Application application, final @NotNull LoadClass classLoader) { this.application = Objects.requireNonNull(application, "Application is required"); - isAndroidXAvailable = checkAndroidXAvailability(classLoader); - isAndroidXScrollViewAvailable = checkAndroidXScrollViewAvailability(classLoader); - } - - private static boolean checkAndroidXAvailability(final @NotNull LoadClass loadClass) { - try { - loadClass.loadClass("androidx.core.view.GestureDetectorCompat"); - return true; - } catch (ClassNotFoundException ignored) { - return false; - } - } - - private static boolean checkAndroidXScrollViewAvailability(final @NotNull LoadClass loadClass) { - try { - loadClass.loadClass("androidx.core.view.ScrollingView"); - return true; - } catch (ClassNotFoundException ignored) { - return false; - } + isAndroidXAvailable = + classLoader.isClassAvailable("androidx.core.view.GestureDetectorCompat", options); + isAndroidXScrollViewAvailable = + classLoader.isClassAvailable("androidx.core.view.ScrollingView", options); } private void startTracking(final @Nullable Window window, final @NotNull Context context) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt index a7cc4e1d20..f57267b642 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt @@ -21,6 +21,7 @@ class ActivityFramesTrackerTest { val activity = mock() val sentryId = SentryId() val loadClass = mock() + val options = SentryAndroidOptions().apply { setDebug(true) } fun getSut(): ActivityFramesTracker { return ActivityFramesTracker(aggregator) @@ -116,16 +117,16 @@ class ActivityFramesTrackerTest { @Test fun `addActivity does not throw if no AndroidX`() { - whenever(fixture.loadClass.loadClass(any())).thenThrow(ClassNotFoundException()) - val sut = ActivityFramesTracker(fixture.loadClass) + whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) + val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) sut.addActivity(fixture.activity) } @Test fun `setMetrics does not throw if no AndroidX`() { - whenever(fixture.loadClass.loadClass(any())).thenThrow(ClassNotFoundException()) - val sut = ActivityFramesTracker(fixture.loadClass) + whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) + val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) sut.setMetrics(fixture.activity, fixture.sentryId) } @@ -133,23 +134,23 @@ class ActivityFramesTrackerTest { @Test fun `setMetrics does not throw if Activity is not added`() { whenever(fixture.aggregator.remove(any())).thenThrow(IllegalArgumentException()) - val sut = ActivityFramesTracker(fixture.loadClass) + val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) sut.setMetrics(fixture.activity, fixture.sentryId) } @Test fun `stop does not throw if no AndroidX`() { - whenever(fixture.loadClass.loadClass(any())).thenThrow(ClassNotFoundException()) - val sut = ActivityFramesTracker(fixture.loadClass) + whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) + val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) sut.stop() } @Test fun `takeMetrics returns null if no AndroidX`() { - whenever(fixture.loadClass.loadClass(any())).thenThrow(ClassNotFoundException()) - val sut = ActivityFramesTracker(fixture.loadClass) + whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) + val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) assertNull(sut.takeMetrics(fixture.sentryId)) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 30c617de2e..77f74590e0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -1,22 +1,16 @@ package io.sentry.android.core -import android.app.Application import android.content.Context import android.os.Bundle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import io.sentry.ILogger import io.sentry.MainEventProcessor import io.sentry.SendCachedEnvelopeFireAndForgetIntegration -import io.sentry.SentryLevel import io.sentry.SentryOptions -import io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration import org.junit.runner.RunWith @@ -31,24 +25,83 @@ import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class AndroidOptionsInitializerTest { - private lateinit var context: Context - private lateinit var file: File + + class Fixture(val context: Context, private val file: File) { + val sentryOptions = SentryAndroidOptions() + lateinit var mockContext: Context + val logger = mock() + + fun initSut( + metadata: Bundle? = null, + hasAppContext: Boolean = true, + useRealContext: Boolean = false, + configureOptions: SentryAndroidOptions.() -> Unit = {}, + configureContext: Context.() -> Unit = {} + ) { + mockContext = if (metadata != null) { + ContextUtilsTest.mockMetaData( + mockContext = ContextUtilsTest.createMockContext(hasAppContext), + metaData = metadata + ) + } else { + ContextUtilsTest.createMockContext(hasAppContext) + } + whenever(mockContext.cacheDir).thenReturn(file) + if (mockContext.applicationContext != null) { + whenever(mockContext.applicationContext.cacheDir).thenReturn(file) + } + mockContext.configureContext() + sentryOptions.configureOptions() + AndroidOptionsInitializer.init( + sentryOptions, if (useRealContext) context else mockContext + ) + } + + fun initSutWithClassLoader( + minApi: Int = 16, + classToLoad: Class<*>? = null + ) { + mockContext = ContextUtilsTest.mockMetaData( + mockContext = ContextUtilsTest.createMockContext(hasAppContext = true), + metaData = Bundle().apply { + putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") + } + ) + sentryOptions.setDebug(true) + AndroidOptionsInitializer.init( + sentryOptions, mockContext, logger, createBuildInfo(minApi), + createClassMock(classToLoad) + ) + } + + private fun createBuildInfo(minApi: Int = 16): IBuildInfoProvider { + val buildInfo = mock() + whenever(buildInfo.sdkInfoVersion).thenReturn(minApi) + return buildInfo + } + + private fun createClassMock(clazz: Class<*>?): LoadClass { + val loadClassMock = mock() + whenever(loadClassMock.loadClass(any(), any())).thenReturn(clazz) + whenever(loadClassMock.isClassAvailable(any(), any())).thenReturn(clazz != null) + return loadClassMock + } + } + + private lateinit var fixture: Fixture @BeforeTest fun `set up`() { - context = ApplicationProvider.getApplicationContext() - file = context.cacheDir + val appContext = ApplicationProvider.getApplicationContext() + fixture = Fixture(appContext, appContext.cacheDir) } @Test fun `logger set to AndroidLogger`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() - - AndroidOptionsInitializer.init(sentryOptions, mockContext) + fixture.initSut() val logger = SentryOptions::class.java.declaredFields.first { it.name == "logger" } logger.isAccessible = true - val loggerField = logger.get(sentryOptions) + val loggerField = logger.get(fixture.sentryOptions) val innerLogger = loggerField.javaClass.declaredFields.first { it.name == "logger" } innerLogger.isAccessible = true assertTrue(innerLogger.get(loggerField) is AndroidLogger) @@ -56,423 +109,229 @@ class AndroidOptionsInitializerTest { @Test fun `AndroidEventProcessor added to processors list`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() - - AndroidOptionsInitializer.init(sentryOptions, mockContext) - val actual = sentryOptions.eventProcessors.any { it is DefaultAndroidEventProcessor } + fixture.initSut() + val actual = + fixture.sentryOptions.eventProcessors.any { it is DefaultAndroidEventProcessor } assertNotNull(actual) } @Test fun `PerformanceAndroidEventProcessor added to processors list`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() - - AndroidOptionsInitializer.init(sentryOptions, mockContext) - val actual = sentryOptions.eventProcessors.any { it is PerformanceAndroidEventProcessor } + fixture.initSut() + val actual = + fixture.sentryOptions.eventProcessors.any { it is PerformanceAndroidEventProcessor } assertNotNull(actual) } @Test fun `MainEventProcessor added to processors list and its the 1st`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() - - AndroidOptionsInitializer.init(sentryOptions, mockContext) - val actual = sentryOptions.eventProcessors.firstOrNull { it is MainEventProcessor } + fixture.initSut() + val actual = fixture.sentryOptions.eventProcessors.firstOrNull { it is MainEventProcessor } assertNotNull(actual) } @Test fun `envelopesDir should be set at initialization`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() + fixture.initSut() - AndroidOptionsInitializer.init(sentryOptions, mockContext) - - assertTrue(sentryOptions.cacheDirPath?.endsWith("${File.separator}cache${File.separator}sentry")!!) + assertTrue( + fixture.sentryOptions.cacheDirPath?.endsWith( + "${File.separator}cache${File.separator}sentry" + )!! + ) } @Test fun `outboxDir should be set at initialization`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() - - AndroidOptionsInitializer.init(sentryOptions, mockContext) + fixture.initSut() - assertTrue(sentryOptions.outboxPath?.endsWith("${File.separator}cache${File.separator}sentry${File.separator}outbox")!!) + assertTrue( + fixture.sentryOptions.outboxPath?.endsWith( + "${File.separator}cache${File.separator}sentry${File.separator}outbox" + )!! + ) } @Test fun `init should set context package name as appInclude`() { - val sentryOptions = SentryAndroidOptions() - - AndroidOptionsInitializer.init(sentryOptions, context) + fixture.initSut(useRealContext = true) // cant mock PackageInfo, its buggy - assertTrue(sentryOptions.inAppIncludes.contains("io.sentry.android.core.test")) + assertTrue(fixture.sentryOptions.inAppIncludes.contains("io.sentry.android.core.test")) } @Test fun `init should set release if empty`() { - val sentryOptions = SentryAndroidOptions() - - AndroidOptionsInitializer.init(sentryOptions, context) + fixture.initSut(useRealContext = true) // cant mock PackageInfo, its buggy - assertTrue(sentryOptions.release!!.startsWith("io.sentry.android.core.test@")) + assertTrue(fixture.sentryOptions.release!!.startsWith("io.sentry.android.core.test@")) } @Test fun `init should not replace options if set on manifest`() { - val sentryOptions = SentryAndroidOptions().apply { - release = "release" - } - - AndroidOptionsInitializer.init(sentryOptions, context) + fixture.initSut(configureOptions = { release = "release" }) - assertEquals("release", sentryOptions.release) + assertEquals("release", fixture.sentryOptions.release) } @Test fun `init should not set context package name if it starts with android package`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = ContextUtilsTest.createMockContext() - whenever(mockContext.packageName).thenReturn("android.context") - - AndroidOptionsInitializer.init(sentryOptions, mockContext) + fixture.initSut(configureContext = { + whenever(packageName).thenReturn("android.context") + }) - assertFalse(sentryOptions.inAppIncludes.contains("android.context")) + assertFalse(fixture.sentryOptions.inAppIncludes.contains("android.context")) } @Test fun `init should set distinct id on start`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = ContextUtilsTest.createMockContext() - - AndroidOptionsInitializer.init(sentryOptions, mockContext) + fixture.initSut() - assertNotNull(sentryOptions.distinctId) { + assertNotNull(fixture.sentryOptions.distinctId) { assertTrue(it.isNotEmpty()) } - val installation = File(context.filesDir, Installation.INSTALLATION) + val installation = File(fixture.context.filesDir, Installation.INSTALLATION) installation.deleteOnExit() } @Test fun `init should set proguard uuid id on start`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithProguardUuid()) - - AndroidOptionsInitializer.init(sentryOptions, mockContext) + fixture.initSut( + Bundle().apply { + putString(ManifestMetadataReader.PROGUARD_UUID, "proguard-uuid") + }, + hasAppContext = false + ) - assertEquals("proguard-uuid", sentryOptions.proguardUuid) + assertEquals("proguard-uuid", fixture.sentryOptions.proguardUuid) } @Test fun `init should set Android transport gate`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() - - AndroidOptionsInitializer.init(sentryOptions, mockContext) + fixture.initSut() - assertNotNull(sentryOptions.transportGate) - assertTrue(sentryOptions.transportGate is AndroidTransportGate) + assertNotNull(fixture.sentryOptions.transportGate) + assertTrue(fixture.sentryOptions.transportGate is AndroidTransportGate) } @Test fun `NdkIntegration will load SentryNdk class and add to the integration list`() { - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) - val logger = mock() - val sentryOptions = SentryAndroidOptions().apply { - setDebug(true) - } + fixture.initSutWithClassLoader(classToLoad = SentryNdk::class.java) - AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(), createClassMock()) - - val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration } + val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration } assertNotNull((actual as NdkIntegration).sentryNdkClass) - - verify(logger, never()).log(eq(SentryLevel.ERROR), any(), any()) - verify(logger, never()).log(eq(SentryLevel.FATAL), any(), any()) } @Test fun `NdkIntegration won't be enabled because API is lower than 16`() { - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) - val logger = mock() - val sentryOptions = SentryAndroidOptions().apply { - setDebug(true) - } - - AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(14), createClassMock()) - - val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration } - assertNull((actual as NdkIntegration).sentryNdkClass) - - verify(logger, never()).log(eq(SentryLevel.ERROR), any(), any()) - verify(logger, never()).log(eq(SentryLevel.FATAL), any(), any()) - } - - @Test - fun `NdkIntegration won't be enabled, it throws linkage error`() { - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) - val logger = mock() - val sentryOptions = SentryAndroidOptions().apply { - setDebug(true) - } + fixture.initSutWithClassLoader(minApi = 14, classToLoad = SentryNdk::class.java) - AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(), createClassMockThrows(UnsatisfiedLinkError())) - - val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration } + val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration } assertNull((actual as NdkIntegration).sentryNdkClass) - - verify(logger).log(eq(SentryLevel.ERROR), any(), any()) } @Test - fun `NdkIntegration won't be enabled, it throws class not found`() { - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) - val logger = mock() - val sentryOptions = SentryAndroidOptions().apply { - setDebug(true) - } + fun `NdkIntegration won't be enabled, if class not found`() { + fixture.initSutWithClassLoader(classToLoad = null) - AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(), createClassMockThrows(ClassNotFoundException())) - - val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration } + val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration } assertNull((actual as NdkIntegration).sentryNdkClass) - - verify(logger).log(eq(SentryLevel.ERROR), any(), any()) - } - - @Test - fun `NdkIntegration won't be enabled, it throws unknown error`() { - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) - val logger = mock() - val sentryOptions = SentryAndroidOptions().apply { - setDebug(true) - } - - AndroidOptionsInitializer.init(sentryOptions, mockContext, logger, createBuildInfo(), createClassMockThrows(RuntimeException())) - - val actual = sentryOptions.integrations.firstOrNull { it is NdkIntegration } - assertNull((actual as NdkIntegration).sentryNdkClass) - - verify(logger).log(eq(SentryLevel.ERROR), any(), any()) } @Test fun `AnrIntegration added to integration list`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() + fixture.initSut() - AndroidOptionsInitializer.init(sentryOptions, mockContext) - val actual = sentryOptions.integrations.firstOrNull { it is AnrIntegration } + val actual = fixture.sentryOptions.integrations.firstOrNull { it is AnrIntegration } assertNotNull(actual) } @Test fun `EnvelopeFileObserverIntegration added to integration list`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() + fixture.initSut() - AndroidOptionsInitializer.init(sentryOptions, mockContext) - val actual = sentryOptions.integrations.firstOrNull { it is EnvelopeFileObserverIntegration } + val actual = + fixture.sentryOptions.integrations.firstOrNull { it is EnvelopeFileObserverIntegration } assertNotNull(actual) } @Test fun `SendCachedEnvelopeFireAndForgetIntegration added to integration list`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = createMockContext() + fixture.initSut() - AndroidOptionsInitializer.init(sentryOptions, mockContext) - val actual = sentryOptions.integrations.firstOrNull { it is SendCachedEnvelopeFireAndForgetIntegration } + val actual = + fixture.sentryOptions.integrations + .firstOrNull { it is SendCachedEnvelopeFireAndForgetIntegration } assertNotNull(actual) } @Test fun `When given Context returns a non null ApplicationContext, uses it`() { - val sentryOptions = SentryAndroidOptions() - val mockApp = mock() - val mockContext = mock() - whenever(mockContext.applicationContext).thenReturn(mockApp) + fixture.initSut() - AndroidOptionsInitializer.init(sentryOptions, mockContext) - assertNotNull(mockContext) + assertNotNull(fixture.mockContext) } @Test fun `When given Context returns a null ApplicationContext is null, keep given Context`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = mock() - whenever(mockContext.applicationContext).thenReturn(null) + fixture.initSut(hasAppContext = false) - AndroidOptionsInitializer.init(sentryOptions, mockContext) - assertNotNull(mockContext) + assertNotNull(fixture.mockContext) } @Test fun `When given Context is not an Application class, do not add ActivityLifecycleIntegration`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = mock() - whenever(mockContext.applicationContext).thenReturn(null) + fixture.initSut(hasAppContext = false) - AndroidOptionsInitializer.init(sentryOptions, mockContext) - val actual = sentryOptions.integrations.firstOrNull { it is ActivityLifecycleIntegration } + val actual = fixture.sentryOptions.integrations + .firstOrNull { it is ActivityLifecycleIntegration } assertNull(actual) } @Test fun `When given Context is not an Application class, do not add UserInteractionIntegration`() { - val sentryOptions = SentryAndroidOptions() - val mockContext = mock() - whenever(mockContext.applicationContext).thenReturn(null) + fixture.initSut(hasAppContext = false) - AndroidOptionsInitializer.init(sentryOptions, mockContext) - val actual = sentryOptions.integrations.firstOrNull { it is UserInteractionIntegration } + val actual = fixture.sentryOptions.integrations + .firstOrNull { it is UserInteractionIntegration } assertNull(actual) } @Test fun `FragmentLifecycleIntegration added to the integration list if available on classpath`() { - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) - val logger = mock() - val sentryOptions = SentryAndroidOptions().apply { - setDebug(true) - } - - AndroidOptionsInitializer.init( - sentryOptions, - mockContext, - logger, - createBuildInfo(), - createClassMock(FragmentLifecycleIntegration::class.java) - ) + fixture.initSutWithClassLoader(classToLoad = FragmentLifecycleIntegration::class.java) - val actual = sentryOptions.integrations.firstOrNull { it is FragmentLifecycleIntegration } + val actual = + fixture.sentryOptions.integrations.firstOrNull { it is FragmentLifecycleIntegration } assertNotNull(actual) - - verify(logger, never()).log(eq(SentryLevel.WARNING), any(), any()) } @Test fun `FragmentLifecycleIntegration won't be enabled, it throws class not found`() { - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) - val logger = mock() - val sentryOptions = SentryAndroidOptions().apply { - setDebug(true) - } - - AndroidOptionsInitializer.init( - sentryOptions, - mockContext, - logger, - createBuildInfo(), - createClassMockThrows( - ClassNotFoundException(), - AndroidOptionsInitializer.SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME - ) - ) + fixture.initSutWithClassLoader(classToLoad = null) - val actual = sentryOptions.integrations.firstOrNull { it is FragmentLifecycleIntegration } + val actual = + fixture.sentryOptions.integrations.firstOrNull { it is FragmentLifecycleIntegration } assertNull(actual) - - verify(logger).log( - eq(SentryLevel.WARNING), - eq("${AndroidOptionsInitializer.SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME} won't be installed as it's not available on the classpath") - ) } @Test fun `SentryTimberIntegration added to the integration list if available on classpath`() { - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) - val logger = mock() - val sentryOptions = SentryAndroidOptions().apply { - setDebug(true) - } - - AndroidOptionsInitializer.init( - sentryOptions, - mockContext, - logger, - createBuildInfo(), - createClassMock(SentryTimberIntegration::class.java) - ) + fixture.initSutWithClassLoader(classToLoad = SentryTimberIntegration::class.java) - val actual = sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } + val actual = + fixture.sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } assertNotNull(actual) - - verify(logger, never()).log(eq(SentryLevel.WARNING), any(), any()) } @Test fun `SentryTimberIntegration won't be enabled, it throws class not found`() { - val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn()) - val logger = mock() - val sentryOptions = SentryAndroidOptions().apply { - setDebug(true) - } + fixture.initSutWithClassLoader(classToLoad = null) - AndroidOptionsInitializer.init( - sentryOptions, - mockContext, - logger, - createBuildInfo(), - createClassMockThrows( - ClassNotFoundException(), - AndroidOptionsInitializer.SENTRY_TIMBER_INTEGRATION_CLASS_NAME - ) - ) - - val actual = sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } + val actual = + fixture.sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } assertNull(actual) - - verify(logger).log( - eq(SentryLevel.WARNING), - eq("${AndroidOptionsInitializer.SENTRY_TIMBER_INTEGRATION_CLASS_NAME} won't be installed as it's not available on the classpath") - ) - } - - private fun createMockContext(): Context { - val mockContext = ContextUtilsTest.createMockContext() - whenever(mockContext.cacheDir).thenReturn(file) - return mockContext - } - - private fun createBundleWithDsn(): Bundle { - return Bundle().apply { - putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") - } - } - - private fun createBundleWithProguardUuid(): Bundle { - return Bundle().apply { - putString(ManifestMetadataReader.PROGUARD_UUID, "proguard-uuid") - } - } - - private fun createBuildInfo(minApi: Int = 16): IBuildInfoProvider { - val buildInfo = mock() - whenever(buildInfo.sdkInfoVersion).thenReturn(minApi) - return buildInfo - } - - private fun createClassMock(clazz: Class<*> = SentryNdk::class.java): LoadClass { - val loadClassMock = mock() - whenever(loadClassMock.loadClass(any())).thenReturn(clazz) - return loadClassMock - } - - private fun createClassMockThrows( - ex: Throwable, - className: String = SENTRY_NDK_CLASS_NAME - ): LoadClass { - val loadClassMock = mock() - whenever(loadClassMock.loadClass(eq(className))).thenThrow(ex) - return loadClassMock } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt index e536ed255f..6c1d3fc824 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt @@ -9,11 +9,10 @@ import android.os.Bundle import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever -import java.io.File import java.io.FileNotFoundException object ContextUtilsTest { - fun mockMetaData(mockContext: Context = createMockContext(), metaData: Bundle): Context { + fun mockMetaData(mockContext: Context = createMockContext(hasAppContext = false), metaData: Bundle): Context { val mockPackageManager = mock() val mockApplicationInfo = mock() val assets = mock() @@ -29,10 +28,9 @@ object ContextUtilsTest { return mockContext } - fun createMockContext(): Context { - val mockApp = mock() - whenever(mockApp.applicationContext).thenReturn(mockApp) - whenever(mockApp.cacheDir).thenReturn(File("")) + fun createMockContext(hasAppContext: Boolean = true): Context { + val mockApp = mock() + whenever(mockApp.applicationContext).thenReturn(if (hasAppContext) mock() else null) return mockApp } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt index 1f4e993695..2777bd088e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt @@ -8,6 +8,7 @@ import android.util.DisplayMetrics import android.view.Window import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never @@ -33,7 +34,11 @@ class UserInteractionIntegrationTest { val window = mock() val loadClass = mock() - fun getSut(callback: Window.Callback? = null): UserInteractionIntegration { + fun getSut( + callback: Window.Callback? = null, + isAndroidXAvailable: Boolean = true + ): UserInteractionIntegration { + whenever(loadClass.isClassAvailable(any(), anyOrNull())).thenReturn(isAndroidXAvailable) whenever(hub.options).thenReturn(options) whenever(window.callback).thenReturn(callback) whenever(activity.window).thenReturn(window) @@ -87,8 +92,7 @@ class UserInteractionIntegrationTest { @Test fun `when androidx is unavailable doesn't register a callback`() { - whenever(fixture.loadClass.loadClass(any())).thenThrow(ClassNotFoundException()) - val sut = fixture.getSut() + val sut = fixture.getSut(isAndroidXAvailable = false) sut.register(fixture.hub, fixture.options) From d3b9429c45b0ed4b5812182dce3913e1fba379e5 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 10 Mar 2022 16:30:07 +0100 Subject: [PATCH 09/20] Enable fragment lifecycle on creation instead of changing ctor params --- CHANGELOG.md | 3 --- .../java/io/sentry/android/core/AndroidOptionsInitializer.java | 2 +- .../io/sentry/android/fragment/FragmentLifecycleIntegration.kt | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69151be51f..c8e8c65585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,6 @@ * Feat: Automatically enable `Timber` and `Fragment` integrations if they are present on the classpath (#1936) -Behaviour change: -[Performance tracking](https://docs.sentry.io/platforms/android/configuration/integrations/fragment/#configure) for fragment's lifecycle is enabled by default now. - ## 5.6.2 ### Various fixes & improvements diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 7de89acabc..3e64b6701b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -168,7 +168,7 @@ private static void installDefaultIntegrations( (Application) context, buildInfoProvider, activityFramesTracker)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); if (loadClass.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options)) { - options.addIntegration(new FragmentLifecycleIntegration((Application) context)); + options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true)); } } else { options diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt index 54f2edca47..dfa2b145cb 100644 --- a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt @@ -20,7 +20,7 @@ class FragmentLifecycleIntegration( Integration, Closeable { - constructor(application: Application) : this(application, true, true) + constructor(application: Application) : this(application, true, false) private lateinit var hub: IHub private lateinit var options: SentryOptions From 4ad10352297fb7958218894bc8949ecb400649f1 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 10 Mar 2022 16:40:30 +0100 Subject: [PATCH 10/20] Enable auto-init for the sample app --- .../src/main/AndroidManifest.xml | 2 +- .../sentry/samples/android/MyApplication.java | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index c6f42d48dc..74276baa9d 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -64,7 +64,7 @@ - + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index 7471892ab2..58c9523f73 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -14,17 +14,17 @@ public void onCreate() { // Example how to initialize the SDK manually which allows access to SentryOptions callbacks. // Make sure you disable the auto init via manifest meta-data: io.sentry.auto-init=false - SentryAndroid.init( - this, - options -> { - /* - use options, for example, to add a beforeSend callback: - - options.setBeforeSend((event, hint) -> { - process event - }); - */ - }); + // SentryAndroid.init( + // this, + // options -> { + // /* + // use options, for example, to add a beforeSend callback: + // + // options.setBeforeSend((event, hint) -> { + // process event + // }); + // */ + // }); } private void strictMode() { From ac44b3873a3d097db32740157c5a3021cac41a61 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 10 Mar 2022 21:11:14 +0100 Subject: [PATCH 11/20] Deduplicate integrations --- .../io/sentry/android/core/SentryAndroid.java | 52 ++++++++-- .../sentry/android/core/SentryAndroidTest.kt | 98 ++++++++++++------- 2 files changed, 108 insertions(+), 42 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index a90229e370..e9ffbcebf4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -4,11 +4,18 @@ import android.os.SystemClock; import io.sentry.DateUtils; import io.sentry.ILogger; +import io.sentry.Integration; import io.sentry.OptionsContainer; import io.sentry.Sentry; import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import io.sentry.android.fragment.FragmentLifecycleIntegration; +import io.sentry.android.timber.SentryTimberIntegration; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; import java.util.Date; +import java.util.Iterator; +import java.util.List; import org.jetbrains.annotations.NotNull; /** Sentry initialization class */ @@ -69,12 +76,13 @@ public static synchronized void init( try { Sentry.init( - OptionsContainer.create(SentryAndroidOptions.class), - options -> { - AndroidOptionsInitializer.init(options, context, logger); - configuration.configure(options); - }, - true); + OptionsContainer.create(SentryAndroidOptions.class), + options -> { + AndroidOptionsInitializer.init(options, context, logger); + configuration.configure(options); + deduplicateIntegrations(options); + }, + true); } catch (IllegalAccessException e) { logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e); @@ -95,4 +103,36 @@ public static synchronized void init( throw new RuntimeException("Failed to initialize Sentry's SDK", e); } } + + /** + * Deduplicate potentially duplicated Fragment and Timber integrations, which can be added + * automatically by our SDK as well as by the user. The user's ones win over ours. + * + * @param options SentryOptions to retrieve integrations from + */ + private static void deduplicateIntegrations(final @NotNull SentryOptions options) { + final List timberIntegrations = new ArrayList<>(); + final List fragmentIntegrations = new ArrayList<>(); + for (final Integration integration : options.getIntegrations()) { + if (integration instanceof FragmentLifecycleIntegration) { + fragmentIntegrations.add(integration); + } else if (integration instanceof SentryTimberIntegration) { + timberIntegrations.add(integration); + } + } + + if (fragmentIntegrations.size() > 1) { + for (int i = 0; i < fragmentIntegrations.size() - 1; i++) { + final Integration integration = fragmentIntegrations.get(i); + options.getIntegrations().remove(integration); + } + } + + if (timberIntegrations.size() > 1) { + for (int i = 0; i < timberIntegrations.size() - 1; i++) { + final Integration integration = timberIntegrations.get(i); + options.getIntegrations().remove(integration); + } + } + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 1f280b86df..3da78a530c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -1,6 +1,7 @@ package io.sentry.android.core import android.os.Bundle +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.eq @@ -10,6 +11,10 @@ import com.nhaarman.mockitokotlin2.verify import io.sentry.ILogger import io.sentry.Sentry import io.sentry.SentryLevel +import io.sentry.SentryLevel.DEBUG +import io.sentry.SentryLevel.FATAL +import io.sentry.android.fragment.FragmentLifecycleIntegration +import io.sentry.android.timber.SentryTimberIntegration import org.junit.runner.RunWith import kotlin.test.BeforeTest import kotlin.test.Test @@ -21,6 +26,28 @@ import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class SentryAndroidTest { + class Fixture { + + fun initSut( + autoInit: Boolean = false, + logger: ILogger? = null, + options: Sentry.OptionsConfiguration? = null + ) { + val metadata = Bundle().apply { + putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") + putBoolean(ManifestMetadataReader.AUTO_INIT, autoInit) + } + val mockContext = ContextUtilsTest.mockMetaData(metaData = metadata) + when { + logger != null -> SentryAndroid.init(mockContext, logger) + options != null -> SentryAndroid.init(mockContext, options) + else -> SentryAndroid.init(mockContext) + } + } + } + + private val fixture = Fixture() + @BeforeTest fun `set up`() { Sentry.close() @@ -31,13 +58,7 @@ class SentryAndroidTest { fun `when auto-init is disabled and user calls init manually, SDK initializes`() { assertFalse(Sentry.isEnabled()) - val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) - - metaData.putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") - metaData.putBoolean(ManifestMetadataReader.AUTO_INIT, false) - - SentryAndroid.init(mockContext) + fixture.initSut() assertTrue(Sentry.isEnabled()) } @@ -46,15 +67,7 @@ class SentryAndroidTest { fun `when auto-init is disabled and user calls init manually with a logger, SDK initializes`() { assertFalse(Sentry.isEnabled()) - val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) - - metaData.putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") - metaData.putBoolean(ManifestMetadataReader.AUTO_INIT, false) - - val logger = mock() - - SentryAndroid.init(mockContext, logger) + fixture.initSut(logger = mock()) assertTrue(Sentry.isEnabled()) } @@ -63,41 +76,28 @@ class SentryAndroidTest { fun `when auto-init is disabled and user calls init manually with configuration handler, options should be set`() { assertFalse(Sentry.isEnabled()) - val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) - - metaData.putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") - metaData.putBoolean(ManifestMetadataReader.AUTO_INIT, false) - var refOptions: SentryAndroidOptions? = null - SentryAndroid.init(mockContext) { options -> - options.anrTimeoutIntervalMillis = 3000 - refOptions = options + fixture.initSut { + it.anrTimeoutIntervalMillis = 3000 + refOptions = it } assertEquals(3000, refOptions!!.anrTimeoutIntervalMillis) - assertTrue(Sentry.isEnabled()) } @Test fun `init won't throw exception`() { - val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) - metaData.putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") - val logger = mock() - SentryAndroid.init(mockContext, logger) + + fixture.initSut(autoInit = true, logger = logger) + verify(logger, never()).log(eq(SentryLevel.FATAL), any(), any()) } @Test fun `set app start if provider is disabled`() { - val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) - metaData.putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") - - SentryAndroid.init(mockContext, mock()) + fixture.initSut(autoInit = true) // done by ActivityLifecycleIntegration so forcing it here AppStartState.getInstance().setAppStartEnd() @@ -105,4 +105,30 @@ class SentryAndroidTest { assertNotNull(AppStartState.getInstance().appStartInterval) } + + @Test + fun `deduplicates fragment and timber integrations`() { + var refOptions: SentryAndroidOptions? = null + + fixture.initSut(autoInit = true) { + it.addIntegration( + FragmentLifecycleIntegration(ApplicationProvider.getApplicationContext()) + ) + + it.addIntegration( + SentryTimberIntegration(minEventLevel = FATAL, minBreadcrumbLevel = DEBUG) + ) + refOptions = it + } + + assertEquals(refOptions!!.integrations.filterIsInstance().size, 1) + val timberIntegration = + refOptions!!.integrations.find { it is SentryTimberIntegration } as SentryTimberIntegration + assertEquals(timberIntegration.minEventLevel, FATAL) + assertEquals(timberIntegration.minBreadcrumbLevel, DEBUG) + + // fragment integration is not auto-installed in the test, since the context is not Application + // but we just verify here that the single integration is preserved + assertEquals(refOptions!!.integrations.filterIsInstance().size, 1) + } } From 7ca325c100c2466e30a4da16a519a482b33c7ab8 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 10 Mar 2022 21:12:08 +0100 Subject: [PATCH 12/20] spotless --- .../io/sentry/android/core/SentryAndroid.java | 15 +++++++-------- .../io/sentry/samples/android/MyApplication.java | 1 - 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index e9ffbcebf4..b2d86b44b2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -14,7 +14,6 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Date; -import java.util.Iterator; import java.util.List; import org.jetbrains.annotations.NotNull; @@ -76,13 +75,13 @@ public static synchronized void init( try { Sentry.init( - OptionsContainer.create(SentryAndroidOptions.class), - options -> { - AndroidOptionsInitializer.init(options, context, logger); - configuration.configure(options); - deduplicateIntegrations(options); - }, - true); + OptionsContainer.create(SentryAndroidOptions.class), + options -> { + AndroidOptionsInitializer.init(options, context, logger); + configuration.configure(options); + deduplicateIntegrations(options); + }, + true); } catch (IllegalAccessException e) { logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e); diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index 58c9523f73..a4a1c5397a 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -2,7 +2,6 @@ import android.app.Application; import android.os.StrictMode; -import io.sentry.android.core.SentryAndroid; /** Apps. main Application. */ public class MyApplication extends Application { From 37474944456a957bb41c9634aed8a4dff57c9c23 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 10 Mar 2022 21:40:43 +0100 Subject: [PATCH 13/20] Dump api --- sentry-android-core/api/sentry-android-core.api | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 689c85e35a..7d2a807427 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -1,5 +1,5 @@ public final class io/sentry/android/core/ActivityFramesTracker { - public fun (Lio/sentry/android/core/LoadClass;)V + public fun (Lio/sentry/android/core/LoadClass;Lio/sentry/SentryOptions;)V public fun addActivity (Landroid/app/Activity;)V public fun setMetrics (Landroid/app/Activity;Lio/sentry/protocol/SentryId;)V public fun stop ()V @@ -82,7 +82,8 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader { public final class io/sentry/android/core/LoadClass { public fun ()V - public fun loadClass (Ljava/lang/String;)Ljava/lang/Class; + public fun isClassAvailable (Ljava/lang/String;Lio/sentry/SentryOptions;)Z + public fun loadClass (Ljava/lang/String;Lio/sentry/SentryOptions;)Ljava/lang/Class; } public final class io/sentry/android/core/NdkIntegration : io/sentry/Integration, java/io/Closeable { From 74b8d7961cec71a3b1f97bb1e2fdec2d5d50a64c Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 14 Mar 2022 08:16:47 +0100 Subject: [PATCH 14/20] Change SentryOptions to ILogger for LoadClass --- .../android/core/ActivityFramesTracker.java | 9 ++++-- .../core/AndroidOptionsInitializer.java | 8 +++--- .../io/sentry/android/core/LoadClass.java | 28 +++++++++++-------- .../android/core/ActivityFramesTrackerTest.kt | 20 ++++++------- .../core/AndroidOptionsInitializerTest.kt | 2 +- .../core/UserInteractionIntegrationTest.kt | 2 +- 6 files changed, 39 insertions(+), 30 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java index 94b7e80c83..e5724104ba 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.util.SparseIntArray; import androidx.core.app.FrameMetricsAggregator; +import io.sentry.ILogger; import io.sentry.SentryOptions; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; @@ -27,14 +28,18 @@ public final class ActivityFramesTracker { activityMeasurements = new ConcurrentHashMap<>(); public ActivityFramesTracker( - final @NotNull LoadClass loadClass, final @NotNull SentryOptions options) { + final @NotNull LoadClass loadClass, final @Nullable ILogger logger) { androidXAvailable = - loadClass.isClassAvailable("androidx.core.app.FrameMetricsAggregator", options); + loadClass.isClassAvailable("androidx.core.app.FrameMetricsAggregator", logger); if (androidXAvailable) { frameMetricsAggregator = new FrameMetricsAggregator(); } } + public ActivityFramesTracker(final @NotNull LoadClass loadClass) { + this(loadClass, null); + } + @TestOnly ActivityFramesTracker(final @Nullable FrameMetricsAggregator frameMetricsAggregator) { this.frameMetricsAggregator = frameMetricsAggregator; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 3e64b6701b..d1637258df 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -116,7 +116,7 @@ static void init( initializeCacheDirs(context, options); final ActivityFramesTracker activityFramesTracker = - new ActivityFramesTracker(loadClass, options); + new ActivityFramesTracker(loadClass, options.getLogger()); installDefaultIntegrations( context, options, buildInfoProvider, loadClass, activityFramesTracker); @@ -143,7 +143,7 @@ private static void installDefaultIntegrations( // because sentry-native move files around and we don't want to watch that. final Class sentryNdkClass = isNdkAvailable(buildInfoProvider) - ? loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options) + ? loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger()) : null; options.addIntegration(new NdkIntegration(sentryNdkClass)); @@ -167,7 +167,7 @@ private static void installDefaultIntegrations( new ActivityLifecycleIntegration( (Application) context, buildInfoProvider, activityFramesTracker)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); - if (loadClass.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options)) { + if (loadClass.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options.getLogger())) { options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true)); } } else { @@ -177,7 +177,7 @@ private static void installDefaultIntegrations( SentryLevel.WARNING, "ActivityLifecycle, FragmentLifecycle and UserInteraction Integrations need an Application class to be installed."); } - if (loadClass.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options)) { + if (loadClass.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options.getLogger())) { options.addIntegration(new SentryTimberIntegration()); } options.addIntegration(new AppComponentsBreadcrumbsIntegration(context)); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java index 92b5cdfa71..f8be6315d4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java @@ -1,5 +1,6 @@ package io.sentry.android.core; +import io.sentry.ILogger; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import org.jetbrains.annotations.NotNull; @@ -12,33 +13,36 @@ public final class LoadClass { * Try to load a class via reflection * * @param clazz the full class name - * @param options an instance of SentryOptions + * @param logger an instance of ILogger * @return a Class if it's available, or null */ public @Nullable Class loadClass( - final @NotNull String clazz, final @Nullable SentryOptions options) { + final @NotNull String clazz, final @Nullable ILogger logger) { try { return Class.forName(clazz); } catch (ClassNotFoundException e) { - if (options != null) { - options.getLogger().log(SentryLevel.WARNING, "Failed to load class " + clazz, e); + if (logger != null) { + logger.log(SentryLevel.WARNING, "Failed to load class " + clazz, e); } } catch (UnsatisfiedLinkError e) { - if (options != null) { - options - .getLogger() - .log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) " + clazz, e); + if (logger != null) { + logger.log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) " + clazz, e); } } catch (Throwable e) { - if (options != null) { - options.getLogger().log(SentryLevel.ERROR, "Failed to initialize " + clazz, e); + if (logger != null) { + logger.log(SentryLevel.ERROR, "Failed to initialize " + clazz, e); } } return null; } public boolean isClassAvailable( - final @NotNull String clazz, final @Nullable SentryOptions options) { - return loadClass(clazz, options) != null; + final @NotNull String clazz, final @Nullable ILogger logger) { + return loadClass(clazz, logger) != null; + } + + public boolean isClassAvailable( + final @NotNull String clazz, final @Nullable SentryOptions options) { + return isClassAvailable(clazz, options != null ? options.getLogger() : null); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt index f57267b642..1a8d2b5c2c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt @@ -7,6 +7,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever +import io.sentry.ILogger import io.sentry.protocol.SentryId import org.junit.runner.RunWith import kotlin.test.Test @@ -21,7 +22,6 @@ class ActivityFramesTrackerTest { val activity = mock() val sentryId = SentryId() val loadClass = mock() - val options = SentryAndroidOptions().apply { setDebug(true) } fun getSut(): ActivityFramesTracker { return ActivityFramesTracker(aggregator) @@ -117,16 +117,16 @@ class ActivityFramesTrackerTest { @Test fun `addActivity does not throw if no AndroidX`() { - whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) - val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) + whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) + val sut = ActivityFramesTracker(fixture.loadClass) sut.addActivity(fixture.activity) } @Test fun `setMetrics does not throw if no AndroidX`() { - whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) - val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) + whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) + val sut = ActivityFramesTracker(fixture.loadClass) sut.setMetrics(fixture.activity, fixture.sentryId) } @@ -134,23 +134,23 @@ class ActivityFramesTrackerTest { @Test fun `setMetrics does not throw if Activity is not added`() { whenever(fixture.aggregator.remove(any())).thenThrow(IllegalArgumentException()) - val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) + val sut = ActivityFramesTracker(fixture.loadClass) sut.setMetrics(fixture.activity, fixture.sentryId) } @Test fun `stop does not throw if no AndroidX`() { - whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) - val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) + whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) + val sut = ActivityFramesTracker(fixture.loadClass) sut.stop() } @Test fun `takeMetrics returns null if no AndroidX`() { - whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) - val sut = ActivityFramesTracker(fixture.loadClass, fixture.options) + whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) + val sut = ActivityFramesTracker(fixture.loadClass) assertNull(sut.takeMetrics(fixture.sentryId)) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 77f74590e0..228665ae62 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -83,7 +83,7 @@ class AndroidOptionsInitializerTest { private fun createClassMock(clazz: Class<*>?): LoadClass { val loadClassMock = mock() whenever(loadClassMock.loadClass(any(), any())).thenReturn(clazz) - whenever(loadClassMock.isClassAvailable(any(), any())).thenReturn(clazz != null) + whenever(loadClassMock.isClassAvailable(any(), any())).thenReturn(clazz != null) return loadClassMock } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt index 2777bd088e..83c6c0b83b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt @@ -38,7 +38,7 @@ class UserInteractionIntegrationTest { callback: Window.Callback? = null, isAndroidXAvailable: Boolean = true ): UserInteractionIntegration { - whenever(loadClass.isClassAvailable(any(), anyOrNull())).thenReturn(isAndroidXAvailable) + whenever(loadClass.isClassAvailable(any(), anyOrNull())).thenReturn(isAndroidXAvailable) whenever(hub.options).thenReturn(options) whenever(window.callback).thenReturn(callback) whenever(activity.window).thenReturn(window) From 1115346ff2015c1255f1fb6fe8898767877534cf Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 14 Mar 2022 08:17:22 +0100 Subject: [PATCH 15/20] spotless --- .../io/sentry/android/core/ActivityFramesTracker.java | 6 ++---- .../src/main/java/io/sentry/android/core/LoadClass.java | 8 +++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java index e5724104ba..d61a33e119 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java @@ -4,7 +4,6 @@ import android.util.SparseIntArray; import androidx.core.app.FrameMetricsAggregator; import io.sentry.ILogger; -import io.sentry.SentryOptions; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import java.util.HashMap; @@ -27,10 +26,9 @@ public final class ActivityFramesTracker { private final @NotNull Map> activityMeasurements = new ConcurrentHashMap<>(); - public ActivityFramesTracker( - final @NotNull LoadClass loadClass, final @Nullable ILogger logger) { + public ActivityFramesTracker(final @NotNull LoadClass loadClass, final @Nullable ILogger logger) { androidXAvailable = - loadClass.isClassAvailable("androidx.core.app.FrameMetricsAggregator", logger); + loadClass.isClassAvailable("androidx.core.app.FrameMetricsAggregator", logger); if (androidXAvailable) { frameMetricsAggregator = new FrameMetricsAggregator(); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java index f8be6315d4..1a2407d8e6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java @@ -16,8 +16,7 @@ public final class LoadClass { * @param logger an instance of ILogger * @return a Class if it's available, or null */ - public @Nullable Class loadClass( - final @NotNull String clazz, final @Nullable ILogger logger) { + public @Nullable Class loadClass(final @NotNull String clazz, final @Nullable ILogger logger) { try { return Class.forName(clazz); } catch (ClassNotFoundException e) { @@ -36,13 +35,12 @@ public final class LoadClass { return null; } - public boolean isClassAvailable( - final @NotNull String clazz, final @Nullable ILogger logger) { + public boolean isClassAvailable(final @NotNull String clazz, final @Nullable ILogger logger) { return loadClass(clazz, logger) != null; } public boolean isClassAvailable( - final @NotNull String clazz, final @Nullable SentryOptions options) { + final @NotNull String clazz, final @Nullable SentryOptions options) { return isClassAvailable(clazz, options != null ? options.getLogger() : null); } } From 5de53358c75c8ed4f1971d4da13ef4077cd83872 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 14 Mar 2022 10:28:37 +0100 Subject: [PATCH 16/20] Check if timber/fragment are on the classpath before filtering --- .../io/sentry/android/core/SentryAndroid.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index b2d86b44b2..b41e57e746 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -17,6 +17,9 @@ import java.util.List; import org.jetbrains.annotations.NotNull; +import static io.sentry.android.core.AndroidOptionsInitializer.SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME; +import static io.sentry.android.core.AndroidOptionsInitializer.SENTRY_TIMBER_INTEGRATION_CLASS_NAME; + /** Sentry initialization class */ public final class SentryAndroid { @@ -77,9 +80,10 @@ public static synchronized void init( Sentry.init( OptionsContainer.create(SentryAndroidOptions.class), options -> { - AndroidOptionsInitializer.init(options, context, logger); + final LoadClass classLoader = new LoadClass(); + AndroidOptionsInitializer.init(options, context, logger, new BuildInfoProvider(), classLoader); configuration.configure(options); - deduplicateIntegrations(options); + deduplicateIntegrations(options, classLoader); }, true); } catch (IllegalAccessException e) { @@ -109,14 +113,23 @@ public static synchronized void init( * * @param options SentryOptions to retrieve integrations from */ - private static void deduplicateIntegrations(final @NotNull SentryOptions options) { + private static void deduplicateIntegrations(final @NotNull SentryOptions options, final @NotNull LoadClass classLoader) { final List timberIntegrations = new ArrayList<>(); final List fragmentIntegrations = new ArrayList<>(); + + final boolean isFragmentAvailable = classLoader.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options); + final boolean isTimberAvailable = classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options); + for (final Integration integration : options.getIntegrations()) { - if (integration instanceof FragmentLifecycleIntegration) { - fragmentIntegrations.add(integration); - } else if (integration instanceof SentryTimberIntegration) { - timberIntegrations.add(integration); + if (isFragmentAvailable) { + if (integration instanceof FragmentLifecycleIntegration) { + fragmentIntegrations.add(integration); + } + } + if (isTimberAvailable) { + if (integration instanceof SentryTimberIntegration) { + timberIntegrations.add(integration); + } } } From 4bef499bf87b66c9b1a27422beb472b2f9f93287 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 14 Mar 2022 10:28:53 +0100 Subject: [PATCH 17/20] Spotless --- .../io/sentry/android/core/SentryAndroid.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index b41e57e746..62ac0f5a72 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -1,5 +1,8 @@ package io.sentry.android.core; +import static io.sentry.android.core.AndroidOptionsInitializer.SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME; +import static io.sentry.android.core.AndroidOptionsInitializer.SENTRY_TIMBER_INTEGRATION_CLASS_NAME; + import android.content.Context; import android.os.SystemClock; import io.sentry.DateUtils; @@ -17,9 +20,6 @@ import java.util.List; import org.jetbrains.annotations.NotNull; -import static io.sentry.android.core.AndroidOptionsInitializer.SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME; -import static io.sentry.android.core.AndroidOptionsInitializer.SENTRY_TIMBER_INTEGRATION_CLASS_NAME; - /** Sentry initialization class */ public final class SentryAndroid { @@ -81,7 +81,8 @@ public static synchronized void init( OptionsContainer.create(SentryAndroidOptions.class), options -> { final LoadClass classLoader = new LoadClass(); - AndroidOptionsInitializer.init(options, context, logger, new BuildInfoProvider(), classLoader); + AndroidOptionsInitializer.init( + options, context, logger, new BuildInfoProvider(), classLoader); configuration.configure(options); deduplicateIntegrations(options, classLoader); }, @@ -113,12 +114,15 @@ public static synchronized void init( * * @param options SentryOptions to retrieve integrations from */ - private static void deduplicateIntegrations(final @NotNull SentryOptions options, final @NotNull LoadClass classLoader) { + private static void deduplicateIntegrations( + final @NotNull SentryOptions options, final @NotNull LoadClass classLoader) { final List timberIntegrations = new ArrayList<>(); final List fragmentIntegrations = new ArrayList<>(); - final boolean isFragmentAvailable = classLoader.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options); - final boolean isTimberAvailable = classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options); + final boolean isFragmentAvailable = + classLoader.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options); + final boolean isTimberAvailable = + classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options); for (final Integration integration : options.getIntegrations()) { if (isFragmentAvailable) { From 2aae3e0f0ed02f7d11131165dbfd45cae70f7ab8 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 14 Mar 2022 10:31:51 +0100 Subject: [PATCH 18/20] Api dump --- sentry-android-core/api/sentry-android-core.api | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 7d2a807427..7a12eff25c 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -1,5 +1,6 @@ public final class io/sentry/android/core/ActivityFramesTracker { - public fun (Lio/sentry/android/core/LoadClass;Lio/sentry/SentryOptions;)V + public fun (Lio/sentry/android/core/LoadClass;)V + public fun (Lio/sentry/android/core/LoadClass;Lio/sentry/ILogger;)V public fun addActivity (Landroid/app/Activity;)V public fun setMetrics (Landroid/app/Activity;Lio/sentry/protocol/SentryId;)V public fun stop ()V @@ -82,8 +83,9 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader { public final class io/sentry/android/core/LoadClass { public fun ()V + public fun isClassAvailable (Ljava/lang/String;Lio/sentry/ILogger;)Z public fun isClassAvailable (Ljava/lang/String;Lio/sentry/SentryOptions;)Z - public fun loadClass (Ljava/lang/String;Lio/sentry/SentryOptions;)Ljava/lang/Class; + public fun loadClass (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/lang/Class; } public final class io/sentry/android/core/NdkIntegration : io/sentry/Integration, java/io/Closeable { From 36a00fe252a99a0c069c74c0adbd7e528ec11241 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 14 Mar 2022 19:02:22 +0100 Subject: [PATCH 19/20] Load class once for fragment and timber integrations --- .../core/AndroidOptionsInitializer.java | 53 +++++++++++++------ .../io/sentry/android/core/SentryAndroid.java | 28 ++++++---- .../core/AndroidOptionsInitializerTest.kt | 14 ++--- .../android/core/SentryInitProviderTest.kt | 5 +- 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index d1637258df..8a032095a2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -32,12 +32,6 @@ @SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references final class AndroidOptionsInitializer { - static final String SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME = - "io.sentry.android.fragment.FragmentLifecycleIntegration"; - - static final String SENTRY_TIMBER_INTEGRATION_CLASS_NAME = - "io.sentry.android.timber.SentryTimberIntegration"; - /** private ctor */ private AndroidOptionsInitializer() {} @@ -51,7 +45,7 @@ static void init(final @NotNull SentryAndroidOptions options, final @NotNull Con Objects.requireNonNull(context, "The application context is required."); Objects.requireNonNull(options, "The options object is required."); - init(options, context, new AndroidLogger()); + init(options, context, new AndroidLogger(), false, false); } /** @@ -60,12 +54,16 @@ static void init(final @NotNull SentryAndroidOptions options, final @NotNull Con * @param options the SentryOptions * @param context the Application context * @param logger the ILogger interface + * @param isFragmentAvailable whether the Fragment integration is available on the classpath + * @param isTimberAvailable whether the Timber integration is available on the classpath */ static void init( final @NotNull SentryAndroidOptions options, @NotNull Context context, - final @NotNull ILogger logger) { - init(options, context, logger, new BuildInfoProvider()); + final @NotNull ILogger logger, + final boolean isFragmentAvailable, + final boolean isTimberAvailable) { + init(options, context, logger, new BuildInfoProvider(), isFragmentAvailable, isTimberAvailable); } /** @@ -75,13 +73,24 @@ static void init( * @param context the Application context * @param logger the ILogger interface * @param buildInfoProvider the IBuildInfoProvider interface + * @param isFragmentAvailable whether the Fragment integration is available on the classpath + * @param isTimberAvailable whether the Timber integration is available on the classpath */ static void init( final @NotNull SentryAndroidOptions options, @NotNull Context context, final @NotNull ILogger logger, - final @NotNull IBuildInfoProvider buildInfoProvider) { - init(options, context, logger, buildInfoProvider, new LoadClass()); + final @NotNull IBuildInfoProvider buildInfoProvider, + final boolean isFragmentAvailable, + final boolean isTimberAvailable) { + init( + options, + context, + logger, + buildInfoProvider, + new LoadClass(), + isFragmentAvailable, + isTimberAvailable); } /** @@ -92,13 +101,17 @@ static void init( * @param logger the ILogger interface * @param buildInfoProvider the IBuildInfoProvider interface * @param loadClass the LoadClass wrapper + * @param isFragmentAvailable whether the Fragment integration is available on the classpath + * @param isTimberAvailable whether the Timber integration is available on the classpath */ static void init( final @NotNull SentryAndroidOptions options, @NotNull Context context, final @NotNull ILogger logger, final @NotNull IBuildInfoProvider buildInfoProvider, - final @NotNull LoadClass loadClass) { + final @NotNull LoadClass loadClass, + final boolean isFragmentAvailable, + final boolean isTimberAvailable) { Objects.requireNonNull(context, "The context is required."); // it returns null if ContextImpl, so let's check for nullability @@ -118,7 +131,13 @@ static void init( final ActivityFramesTracker activityFramesTracker = new ActivityFramesTracker(loadClass, options.getLogger()); installDefaultIntegrations( - context, options, buildInfoProvider, loadClass, activityFramesTracker); + context, + options, + buildInfoProvider, + loadClass, + activityFramesTracker, + isFragmentAvailable, + isTimberAvailable); readDefaultOptionValues(options, context); @@ -133,7 +152,9 @@ private static void installDefaultIntegrations( final @NotNull SentryOptions options, final @NotNull IBuildInfoProvider buildInfoProvider, final @NotNull LoadClass loadClass, - final @NotNull ActivityFramesTracker activityFramesTracker) { + final @NotNull ActivityFramesTracker activityFramesTracker, + final boolean isFragmentAvailable, + final boolean isTimberAvailable) { options.addIntegration( new SendCachedEnvelopeFireAndForgetIntegration( @@ -167,7 +188,7 @@ private static void installDefaultIntegrations( new ActivityLifecycleIntegration( (Application) context, buildInfoProvider, activityFramesTracker)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); - if (loadClass.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options.getLogger())) { + if (isFragmentAvailable) { options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true)); } } else { @@ -177,7 +198,7 @@ private static void installDefaultIntegrations( SentryLevel.WARNING, "ActivityLifecycle, FragmentLifecycle and UserInteraction Integrations need an Application class to be installed."); } - if (loadClass.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options.getLogger())) { + if (isTimberAvailable) { options.addIntegration(new SentryTimberIntegration()); } options.addIntegration(new AppComponentsBreadcrumbsIntegration(context)); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 62ac0f5a72..dc65b632db 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -1,8 +1,5 @@ package io.sentry.android.core; -import static io.sentry.android.core.AndroidOptionsInitializer.SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME; -import static io.sentry.android.core.AndroidOptionsInitializer.SENTRY_TIMBER_INTEGRATION_CLASS_NAME; - import android.content.Context; import android.os.SystemClock; import io.sentry.DateUtils; @@ -28,6 +25,12 @@ public final class SentryAndroid { // SystemClock.uptimeMillis() isn't affected by phone provider or clock changes. private static final long appStart = SystemClock.uptimeMillis(); + static final String SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME = + "io.sentry.android.fragment.FragmentLifecycleIntegration"; + + static final String SENTRY_TIMBER_INTEGRATION_CLASS_NAME = + "io.sentry.android.timber.SentryTimberIntegration"; + private SentryAndroid() {} /** @@ -81,10 +84,15 @@ public static synchronized void init( OptionsContainer.create(SentryAndroidOptions.class), options -> { final LoadClass classLoader = new LoadClass(); + final boolean isFragmentAvailable = + classLoader.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options); + final boolean isTimberAvailable = + classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options); + AndroidOptionsInitializer.init( - options, context, logger, new BuildInfoProvider(), classLoader); + options, context, logger, isFragmentAvailable, isTimberAvailable); configuration.configure(options); - deduplicateIntegrations(options, classLoader); + deduplicateIntegrations(options, isFragmentAvailable, isTimberAvailable); }, true); } catch (IllegalAccessException e) { @@ -115,15 +123,13 @@ public static synchronized void init( * @param options SentryOptions to retrieve integrations from */ private static void deduplicateIntegrations( - final @NotNull SentryOptions options, final @NotNull LoadClass classLoader) { + final @NotNull SentryOptions options, + final boolean isFragmentAvailable, + final boolean isTimberAvailable) { + final List timberIntegrations = new ArrayList<>(); final List fragmentIntegrations = new ArrayList<>(); - final boolean isFragmentAvailable = - classLoader.isClassAvailable(SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME, options); - final boolean isTimberAvailable = - classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options); - for (final Integration integration : options.getIntegrations()) { if (isFragmentAvailable) { if (integration instanceof FragmentLifecycleIntegration) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 228665ae62..ff23d95fd4 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -59,7 +59,9 @@ class AndroidOptionsInitializerTest { fun initSutWithClassLoader( minApi: Int = 16, - classToLoad: Class<*>? = null + classToLoad: Class<*>? = null, + isFragmentAvailable: Boolean = false, + isTimberAvailable: Boolean = false ) { mockContext = ContextUtilsTest.mockMetaData( mockContext = ContextUtilsTest.createMockContext(hasAppContext = true), @@ -70,7 +72,7 @@ class AndroidOptionsInitializerTest { sentryOptions.setDebug(true) AndroidOptionsInitializer.init( sentryOptions, mockContext, logger, createBuildInfo(minApi), - createClassMock(classToLoad) + createClassMock(classToLoad), isFragmentAvailable, isTimberAvailable ) } @@ -301,7 +303,7 @@ class AndroidOptionsInitializerTest { @Test fun `FragmentLifecycleIntegration added to the integration list if available on classpath`() { - fixture.initSutWithClassLoader(classToLoad = FragmentLifecycleIntegration::class.java) + fixture.initSutWithClassLoader(isFragmentAvailable = true) val actual = fixture.sentryOptions.integrations.firstOrNull { it is FragmentLifecycleIntegration } @@ -310,7 +312,7 @@ class AndroidOptionsInitializerTest { @Test fun `FragmentLifecycleIntegration won't be enabled, it throws class not found`() { - fixture.initSutWithClassLoader(classToLoad = null) + fixture.initSutWithClassLoader(isFragmentAvailable = false) val actual = fixture.sentryOptions.integrations.firstOrNull { it is FragmentLifecycleIntegration } @@ -319,7 +321,7 @@ class AndroidOptionsInitializerTest { @Test fun `SentryTimberIntegration added to the integration list if available on classpath`() { - fixture.initSutWithClassLoader(classToLoad = SentryTimberIntegration::class.java) + fixture.initSutWithClassLoader(isTimberAvailable = true) val actual = fixture.sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } @@ -328,7 +330,7 @@ class AndroidOptionsInitializerTest { @Test fun `SentryTimberIntegration won't be enabled, it throws class not found`() { - fixture.initSutWithClassLoader(classToLoad = null) + fixture.initSutWithClassLoader(isTimberAvailable = false) val actual = fixture.sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryInitProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryInitProviderTest.kt index 59a665b97b..a54f39e568 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryInitProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryInitProviderTest.kt @@ -4,8 +4,6 @@ import android.content.Context import android.content.pm.ProviderInfo import android.os.Bundle import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.nhaarman.mockitokotlin2.mock -import io.sentry.ILogger import io.sentry.Sentry import io.sentry.test.callMethod import org.junit.runner.RunWith @@ -129,13 +127,12 @@ class SentryInitProviderTest { @Test fun `when applicationId is defined, ndk in meta-data is set to false, NDK doesnt initialize`() { val sentryOptions = SentryAndroidOptions() - val mockLogger = mock() val metaData = Bundle() val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) metaData.putBoolean(ManifestMetadataReader.NDK_ENABLE, false) - AndroidOptionsInitializer.init(sentryOptions, mockContext, mockLogger) + AndroidOptionsInitializer.init(sentryOptions, mockContext) assertFalse(sentryOptions.isEnableNdk) } From eecad24717a679fa46e1df70bf316f93bccd0016 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 14 Mar 2022 21:50:45 +0100 Subject: [PATCH 20/20] Update sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java Co-authored-by: Bruno Garcia --- .../src/main/java/io/sentry/android/core/LoadClass.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java index 1a2407d8e6..6401945cab 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java @@ -21,7 +21,7 @@ public final class LoadClass { return Class.forName(clazz); } catch (ClassNotFoundException e) { if (logger != null) { - logger.log(SentryLevel.WARNING, "Failed to load class " + clazz, e); + logger.log(SentryLevel.DEBUG, "Class not available:" + clazz, e); } } catch (UnsatisfiedLinkError e) { if (logger != null) {