Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Timber and Fragment integrations if they are present on the classpath #1936

Merged
merged 22 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
romtsn marked this conversation as resolved.
Show resolved Hide resolved

## 5.6.2

### Various fixes & improvements
Expand Down
5 changes: 5 additions & 0 deletions sentry-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
}

// needed because of Kotlin 1.4.x
Expand Down Expand Up @@ -78,6 +79,8 @@ tasks.withType<JavaCompile>().configureEach {

dependencies {
api(projects.sentry)
compileOnly(projects.sentryAndroidFragment)
compileOnly(projects.sentryAndroidTimber)

// lifecycle processor, session tracking
implementation(Config.Libs.lifecycleProcess)
Expand All @@ -102,4 +105,6 @@ dependencies {
testImplementation(Config.TestLibs.mockitoInline)
testImplementation(Config.TestLibs.awaitility)
testImplementation(projects.sentryTestSupport)
testImplementation(projects.sentryAndroidFragment)
testImplementation(projects.sentryAndroidTimber)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
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.Objects;
import java.io.BufferedInputStream;
import java.io.File;
Expand All @@ -30,6 +32,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() {}

Expand Down Expand Up @@ -155,12 +163,18 @@ 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));
romtsn marked this conversation as resolved.
Show resolved Hide resolved
}
} 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());
romtsn marked this conversation as resolved.
Show resolved Hide resolved
}
options.addIntegration(new AppComponentsBreadcrumbsIntegration(context));
options.addIntegration(new SystemEventsBreadcrumbsIntegration(context));
Expand Down Expand Up @@ -277,4 +291,23 @@ 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);
romtsn marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ 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
import java.io.File
import java.lang.RuntimeException
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -336,6 +337,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<ILogger>()
val sentryOptions = SentryAndroidOptions().apply {
setDebug(true)
}
romtsn marked this conversation as resolved.
Show resolved Hide resolved

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<String>(), any())
}

@Test
fun `FragmentLifecycleIntegration won't be enabled, it throws class not found`() {
val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn())
val logger = mock<ILogger>()
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("${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<ILogger>()
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<String>(), any())
}

@Test
fun `SentryTimberIntegration won't be enabled, it throws class not found`() {
val mockContext = ContextUtilsTest.mockMetaData(metaData = createBundleWithDsn())
val logger = mock<ILogger>()
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("${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)
Expand Down Expand Up @@ -366,9 +467,12 @@ 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<LoadClass>()
whenever(loadClassMock.loadClass(eq(SENTRY_NDK_CLASS_NAME))).thenThrow(ex)
whenever(loadClassMock.loadClass(eq(className))).thenThrow(ex)
return loadClassMock
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FragmentLifecycleIntegration(
Integration,
Closeable {

constructor(application: Application) : this(application, true, false)
constructor(application: Application) : this(application, true, true)
romtsn marked this conversation as resolved.
Show resolved Hide resolved

private lateinit var hub: IHub
private lateinit var options: SentryOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
});
*/
romtsn marked this conversation as resolved.
Show resolved Hide resolved
});
}

Expand Down