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

feat: Add breadcrumbs auto-capturing for UI events #1876

Merged
merged 26 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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 buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ object Config {
val lifecycleProcess = "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
val lifecycleCommonJava8 = "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
val androidxCore = "androidx.core:core:1.3.2"
val androidxRecylerView = "androidx.recyclerview:recyclerview:1.2.1"

val slf4jApi = "org.slf4j:slf4j-api:1.7.30"
val logbackVersion = "1.2.9"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* Android Options initializer, it reads configurations from AndroidManifest and sets to the
* SentryOptions. It also adds default values for some fields.
*/
@SuppressWarnings("Convert2MethodRef")
romtsn marked this conversation as resolved.
Show resolved Hide resolved
final class AndroidOptionsInitializer {

/** private ctor */
Expand Down Expand Up @@ -153,12 +154,14 @@ private static void installDefaultIntegrations(
options.addIntegration(
new ActivityLifecycleIntegration(
(Application) context, buildInfoProvider, activityFramesTracker));
options.addIntegration(
new UserInteractionIntegration((Application) context, buildInfoProvider, loadClass));
} else {
options
.getLogger()
.log(
SentryLevel.WARNING,
"ActivityBreadcrumbsIntegration needs an Application class to be installed.");
"ActivityLifecycle and UserInteraction Integrations need an Application class to be installed.");
}
options.addIntegration(new AppComponentsBreadcrumbsIntegration(context));
options.addIntegration(new SystemEventsBreadcrumbsIntegration(context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ final class ManifestMetadataReader {
static final String BREADCRUMBS_APP_LIFECYCLE_ENABLE = "io.sentry.breadcrumbs.app-lifecycle";
static final String BREADCRUMBS_SYSTEM_EVENTS_ENABLE = "io.sentry.breadcrumbs.system-events";
static final String BREADCRUMBS_APP_COMPONENTS_ENABLE = "io.sentry.breadcrumbs.app-components";
static final String BREADCRUMBS_USER_INTERACTION_ENABLE = "io.sentry.breadcrumbs.user-interaction";

static final String UNCAUGHT_EXCEPTION_HANDLER_ENABLE =
"io.sentry.uncaught-exception-handler.enable";
Expand Down Expand Up @@ -175,6 +176,13 @@ static void applyMetadata(
BREADCRUMBS_APP_COMPONENTS_ENABLE,
options.isEnableAppComponentBreadcrumbs()));

options.setEnableAppComponentBreadcrumbs(
readBool(
metadata,
logger,
BREADCRUMBS_USER_INTERACTION_ENABLE,
options.isEnableUserInteractionBreadcrumbs()));

options.setEnableUncaughtExceptionHandler(
readBool(
metadata,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public final class SentryAndroidOptions extends SentryOptions {
/** Enable or disable automatic breadcrumbs for App Components Using ComponentCallbacks */
private boolean enableAppComponentBreadcrumbs = true;

/** Enable or disable automatic breadcrumbs for User interactions Using Window.Callback */
private boolean enableUserInteractionBreadcrumbs = true;

/**
* Enables the Auto instrumentation for Activity lifecycle tracing.
*
Expand Down Expand Up @@ -189,6 +192,14 @@ public void setEnableAppComponentBreadcrumbs(boolean enableAppComponentBreadcrum
this.enableAppComponentBreadcrumbs = enableAppComponentBreadcrumbs;
}

public boolean isEnableUserInteractionBreadcrumbs() {
return enableUserInteractionBreadcrumbs;
}

public void setEnableUserInteractionBreadcrumbs(boolean enableUserInteractionBreadcrumbs) {
this.enableUserInteractionBreadcrumbs = enableUserInteractionBreadcrumbs;
}

/**
* Enable or disable all the automatic breadcrumbs
*
Expand All @@ -199,6 +210,7 @@ public void enableAllAutoBreadcrumbs(boolean enable) {
enableAppComponentBreadcrumbs = enable;
enableSystemEventBreadcrumbs = enable;
enableAppLifecycleBreadcrumbs = enable;
enableUserInteractionBreadcrumbs = enable;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package io.sentry.android.core;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.view.Window;
import io.sentry.IHub;
import io.sentry.Integration;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.gestures.NoOpWindowCallback;
import io.sentry.android.core.gestures.SentryGestureListener;
import io.sentry.android.core.gestures.SentryWindowCallback;
import io.sentry.util.Objects;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class UserInteractionIntegration
implements Integration, Closeable, Application.ActivityLifecycleCallbacks {

private final @NotNull Application application;
private @Nullable IHub hub;
private @Nullable SentryAndroidOptions options;

private final boolean isAllActivityCallbacksAvailable;
private final boolean isAndroidXAvailable;

public UserInteractionIntegration(
final @NotNull Application application,
final @NotNull IBuildInfoProvider buildInfoProvider,
final @NotNull LoadClass classLoader
) {
this.application = Objects.requireNonNull(application, "Application is required");
Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required");

isAndroidXAvailable = checkAndroidXAvailability(classLoader);
isAllActivityCallbacksAvailable =
buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.Q;
romtsn marked this conversation as resolved.
Show resolved Hide resolved
}

private static boolean checkAndroidXAvailability(final @NotNull LoadClass loadClass) {
try {
loadClass.loadClass("androidx.core.view.GestureDetectorCompat");
return true;
} catch (ClassNotFoundException ignored) {
return false;
}
}
romtsn marked this conversation as resolved.
Show resolved Hide resolved

private void startTracking(final @Nullable Window window, final @NotNull Context context) {
if (window == null) {
if (options != null) {
options
.getLogger()
.log(SentryLevel.INFO, "Window was null in startTracking");
}
return;
}

if (hub != null && options != null) {
Window.Callback delegate = window.getCallback();
if (delegate == null) {
delegate = new NoOpWindowCallback();
}

final SentryGestureListener gestureListener =
new SentryGestureListener(new WeakReference<>(window), hub, options);
window.setCallback(new SentryWindowCallback(delegate, context, gestureListener, options));
}
}

private void stopTracking(final @Nullable Window window) {
if (window == null) {
if (options != null) {
options
.getLogger()
.log(SentryLevel.INFO, "Window was null in stopTracking");
}
return;
}

final Window.Callback current = window.getCallback();
if (current instanceof SentryWindowCallback) {
if (((SentryWindowCallback) current).getDelegate() instanceof NoOpWindowCallback) {
window.setCallback(null);
} else {
window.setCallback(((SentryWindowCallback) current).getDelegate());
}
}
}

@Override
public void onActivityPreCreated(@NotNull Activity activity,
@Nullable Bundle savedInstanceState) {
if (isAllActivityCallbacksAvailable) {
startTracking(activity.getWindow(), activity);
}
}

@Override
public void onActivityCreated(@NotNull Activity activity,
@Nullable Bundle bundle) {

}

@Override
public void onActivityStarted(@NotNull Activity activity) {

}

@Override
public void onActivityResumed(@NotNull Activity activity) {
if (!isAllActivityCallbacksAvailable) {
startTracking(activity.getWindow(), activity);
}
}

@Override
public void onActivityPaused(@NotNull Activity activity) {
if (!isAllActivityCallbacksAvailable) {
stopTracking(activity.getWindow());
}
}

@Override
public void onActivityStopped(@NotNull Activity activity) {
romtsn marked this conversation as resolved.
Show resolved Hide resolved

}

@Override
public void onActivitySaveInstanceState(@NotNull Activity activity,
@NotNull Bundle bundle) {

}

@Override
public void onActivityDestroyed(@NotNull Activity activity) {

}

@Override
public void register(@NotNull IHub hub, @NotNull SentryOptions options) {
this.options =
Objects.requireNonNull(
(options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
"SentryAndroidOptions is required");

this.hub = Objects.requireNonNull(hub, "Hub is required");

this.options
.getLogger()
.log(
SentryLevel.DEBUG,
"UserInteractionIntegration enabled: %s",
this.options.isEnableUserInteractionBreadcrumbs());

if (this.options.isEnableUserInteractionBreadcrumbs()) {
if (isAndroidXAvailable) {
application.registerActivityLifecycleCallbacks(this);
this.options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration installed.");
} else {
options
.getLogger()
.log(SentryLevel.INFO,
"androidx.core is not available, UserInteractionIntegration won't be installed");
}
}
}

@Override
public void close() throws IOException {
application.unregisterActivityLifecycleCallbacks(this);

if (options != null) {
options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration removed.");
}
}
}
Loading