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 14 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

* Feat: Automatically enable `Timber` and `Fragment` integrations if they are present on the classpath (#1936)
* Fix: If transaction or span is finished, do not allow to mutate (#1940)

## 5.6.2
Expand Down
5 changes: 3 additions & 2 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
public final class io/sentry/android/core/ActivityFramesTracker {
public fun <init> (Lio/sentry/android/core/LoadClass;)V
public fun <init> (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
Expand Down Expand Up @@ -82,7 +82,8 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader {

public final class io/sentry/android/core/LoadClass {
public fun <init> ()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 {
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 @@ -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;
Expand All @@ -25,8 +26,10 @@ public final class ActivityFramesTracker {
private final @NotNull Map<SentryId, Map<String, @NotNull MeasurementValue>>
activityMeasurements = new ConcurrentHashMap<>();

public ActivityFramesTracker(final @NotNull LoadClass loadClass) {
androidXAvailable = checkAndroidXAvailability(loadClass);
public ActivityFramesTracker(
final @NotNull LoadClass loadClass, final @NotNull SentryOptions options) {
romtsn marked this conversation as resolved.
Show resolved Hide resolved
androidXAvailable =
loadClass.isClassAvailable("androidx.core.app.FrameMetricsAggregator", options);
if (androidXAvailable) {
frameMetricsAggregator = new FrameMetricsAggregator();
}
Expand All @@ -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;
}
Expand Down
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 @@ -107,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);

Expand All @@ -132,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
Expand All @@ -155,12 +167,18 @@ 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)) {
options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true));
}
} 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 (loadClass.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options)) {
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 @@ -257,24 +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;
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@
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.List;
import org.jetbrains.annotations.NotNull;

/** Sentry initialization class */
Expand Down Expand Up @@ -73,6 +79,7 @@ public static synchronized void init(
options -> {
AndroidOptionsInitializer.init(options, context, logger);
configuration.configure(options);
deduplicateIntegrations(options);
},
true);
} catch (IllegalAccessException e) {
Expand All @@ -95,4 +102,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<Integration> timberIntegrations = new ArrayList<>();
final List<Integration> fragmentIntegrations = new ArrayList<>();
for (final Integration integration : options.getIntegrations()) {
romtsn marked this conversation as resolved.
Show resolved Hide resolved
if (integration instanceof FragmentLifecycleIntegration) {
fragmentIntegrations.add(integration);
} else if (integration instanceof SentryTimberIntegration) {
timberIntegrations.add(integration);
}
romtsn marked this conversation as resolved.
Show resolved Hide resolved
}

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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ActivityFramesTrackerTest {
val activity = mock<Activity>()
val sentryId = SentryId()
val loadClass = mock<LoadClass>()
val options = SentryAndroidOptions().apply { setDebug(true) }

fun getSut(): ActivityFramesTracker {
return ActivityFramesTracker(aggregator)
Expand Down Expand Up @@ -116,40 +117,40 @@ 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)
}

@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))
}
Expand Down
Loading