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 Android View Hierarchy support #2440

Merged
merged 29 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8682a9e
Add intial implemention for Android View Hierarchy support
markushi Dec 22, 2022
5e82c3a
Move vh attachment serialization to processor, add tests
markushi Dec 29, 2022
df56bd1
Merge branch 'main' into feat/view-hierarchy
markushi Dec 29, 2022
426d75a
Add changelog
markushi Dec 29, 2022
603c073
Switch sentry-compose-helper to kotlin mutliplatform
markushi Dec 29, 2022
ccc324b
Extract Screenshot activity callbacks to CurrentActivityIntegration
markushi Dec 30, 2022
5b6d756
Fix failing compose tests
markushi Dec 30, 2022
152cf78
Merge branch 'main' into feat/view-hierarchy
markushi Dec 30, 2022
0fd2993
Add missing integration tests for view hierarchy
markushi Jan 3, 2023
e5ab60b
Merge branch 'main' into feat/view-hierarchy
markushi Jan 3, 2023
95b543f
Fix Changelog
markushi Jan 3, 2023
ed189f8
Adapt modifiers / UTF-8 charset handling
markushi Jan 3, 2023
c67d132
Safeguard serialization of view hierarchy
markushi Jan 3, 2023
6f6a909
Update sentry/src/main/java/io/sentry/Hint.java
markushi Jan 4, 2023
1574530
Update sentry/src/main/java/io/sentry/protocol/ViewHierarchy.java
markushi Jan 4, 2023
b4463cf
Update CHANGELOG.md
markushi Jan 4, 2023
2084f33
Merge branch 'main' into feat/view-hierarchy
markushi Jan 4, 2023
cbffc5c
Improve structure based on PR comments
markushi Jan 4, 2023
9f65ca6
Add bytesFactory field to attachment
markushi Jan 5, 2023
040e1a0
Use visibility attribute on Android
markushi Jan 10, 2023
52af78f
Move ScreenshotEventProcessor init
markushi Jan 10, 2023
6077f28
Set proper attachment type for view hierarchy
markushi Jan 10, 2023
2c0efab
Re-use attachment.getBytes() result
markushi Jan 10, 2023
7ce3c98
Move View Hierarchy serialization from processor to SentryEnvelopeItem
markushi Jan 10, 2023
e219a6f
Merge branch 'main' into feat/view-hierarchy
markushi Jan 10, 2023
bbe99b4
Remove unused field
markushi Jan 11, 2023
3d22c9f
Fix don't lookup resource name when view id is not set
markushi Jan 11, 2023
331fb96
Fix typo
markushi Jan 12, 2023
c94745d
Fix do not look up resource names for generated view ids
markushi Jan 12, 2023
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Add Android View Hierarchy support ([#2440](https://github.com/getsentry/sentry-java/pull/2440))
markushi marked this conversation as resolved.
Show resolved Hide resolved

## 6.11.0

### Features
Expand Down
33 changes: 23 additions & 10 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ public class io/sentry/android/core/CurrentActivityHolder {
public fun setActivity (Landroid/app/Activity;)V
}

public final class io/sentry/android/core/CurrentActivityIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable {
public fun <init> (Landroid/app/Application;)V
public fun close ()V
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityDestroyed (Landroid/app/Activity;)V
public fun onActivityPaused (Landroid/app/Activity;)V
public fun onActivityResumed (Landroid/app/Activity;)V
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityStarted (Landroid/app/Activity;)V
public fun onActivityStopped (Landroid/app/Activity;)V
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
}

public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : io/sentry/Integration, java/io/Closeable {
public fun <init> ()V
public fun close ()V
Expand Down Expand Up @@ -121,16 +134,8 @@ public final class io/sentry/android/core/PhoneStateBreadcrumbsIntegration : io/
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/core/ScreenshotEventProcessor : android/app/Application$ActivityLifecycleCallbacks, io/sentry/EventProcessor, java/io/Closeable {
public fun <init> (Landroid/app/Application;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V
public fun close ()V
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityDestroyed (Landroid/app/Activity;)V
public fun onActivityPaused (Landroid/app/Activity;)V
public fun onActivityResumed (Landroid/app/Activity;)V
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityStarted (Landroid/app/Activity;)V
public fun onActivityStopped (Landroid/app/Activity;)V
public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor {
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
}

Expand All @@ -152,6 +157,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isAnrEnabled ()Z
public fun isAnrReportInDebug ()Z
public fun isAttachScreenshot ()Z
public fun isAttachViewHierarchy ()Z
public fun isCollectAdditionalContext ()Z
public fun isEnableActivityLifecycleBreadcrumbs ()Z
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
Expand All @@ -164,6 +170,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setAnrReportInDebug (Z)V
public fun setAnrTimeoutIntervalMillis (J)V
public fun setAttachScreenshot (Z)V
public fun setAttachViewHierarchy (Z)V
public fun setCollectAdditionalContext (Z)V
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
Expand Down Expand Up @@ -235,6 +242,12 @@ public final class io/sentry/android/core/UserInteractionIntegration : android/a
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/EventProcessor {
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
public static fun snapshotViewHierarchy (Landroid/view/View;)Lio/sentry/protocol/ViewHierarchy;
}

public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
public fun getDirectory ()Ljava/io/File;
Expand Down
1 change: 1 addition & 0 deletions sentry-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ dependencies {
testImplementation(projects.sentryAndroidTimber)
testImplementation(projects.sentryComposeHelper)
testImplementation(projects.sentryAndroidNdk)
testRuntimeOnly(Config.Libs.composeUi)
testRuntimeOnly(Config.Libs.timber)
testRuntimeOnly(Config.Libs.fragment)
}
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,21 @@ private static void installDefaultIntegrations(
options.addIntegration(
new ActivityLifecycleIntegration(
(Application) context, buildInfoProvider, activityFramesTracker));
options.addIntegration(new CurrentActivityIntegration((Application) context));
options.addIntegration(new UserInteractionIntegration((Application) context, loadClass));
if (isFragmentAvailable) {
options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true));
}
options.addEventProcessor(
new ScreenshotEventProcessor((Application) context, options, buildInfoProvider));
options.addEventProcessor(new ScreenshotEventProcessor(options, buildInfoProvider));
markushi marked this conversation as resolved.
Show resolved Hide resolved
markushi marked this conversation as resolved.
Show resolved Hide resolved
} else {
options
.getLogger()
.log(
SentryLevel.WARNING,
"ActivityLifecycle, FragmentLifecycle and UserInteraction Integrations need an Application class to be installed.");
}
options.addEventProcessor(new ViewHierarchyEventProcessor(options));

if (isTimberAvailable) {
options.addIntegration(new SentryTimberIntegration());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.sentry.android.core;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.NonNull;
import io.sentry.IHub;
import io.sentry.Integration;
import io.sentry.SentryOptions;
import io.sentry.util.Objects;
import java.io.Closeable;
import java.io.IOException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class CurrentActivityIntegration
implements Integration, Closeable, Application.ActivityLifecycleCallbacks {

private final @NotNull Application application;

public CurrentActivityIntegration(final @NotNull Application application) {
this.application = Objects.requireNonNull(application, "Application is required");
}

@Override
public void register(@NotNull IHub hub, @NotNull SentryOptions options) {
application.registerActivityLifecycleCallbacks(this);
}

@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
setCurrentActivity(activity);
}

@Override
public void onActivityStarted(@NonNull Activity activity) {
setCurrentActivity(activity);
}

@Override
public void onActivityResumed(@NonNull Activity activity) {
setCurrentActivity(activity);
}

@Override
public void onActivityPaused(@NonNull Activity activity) {
cleanCurrentActivity(activity);
}

@Override
public void onActivityStopped(@NonNull Activity activity) {
cleanCurrentActivity(activity);
}

@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}

@Override
public void onActivityDestroyed(@NonNull Activity activity) {
cleanCurrentActivity(activity);
}

@Override
public void close() throws IOException {
application.unregisterActivityLifecycleCallbacks(this);
CurrentActivityHolder.getInstance().clearActivity();
}

private void cleanCurrentActivity(final @NotNull Activity activity) {
if (CurrentActivityHolder.getInstance().getActivity() == activity) {
CurrentActivityHolder.getInstance().clearActivity();
}
}

private void setCurrentActivity(final @NotNull Activity activity) {
CurrentActivityHolder.getInstance().setActivity(activity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ final class ManifestMetadataReader {
static final String IDLE_TIMEOUT = "io.sentry.traces.idle-timeout";

static final String ATTACH_SCREENSHOT = "io.sentry.attach-screenshot";
static final String ATTACH_VIEW_HIERARCHY = "io.sentry.attach-view-hierarchy";
static final String CLIENT_REPORTS_ENABLE = "io.sentry.send-client-reports";
static final String COLLECT_ADDITIONAL_CONTEXT = "io.sentry.additional-context";

Expand Down Expand Up @@ -220,6 +221,9 @@ static void applyMetadata(
options.setAttachScreenshot(
readBool(metadata, logger, ATTACH_SCREENSHOT, options.isAttachScreenshot()));

options.setAttachViewHierarchy(
readBool(metadata, logger, ATTACH_VIEW_HIERARCHY, options.isAttachViewHierarchy()));

options.setSendClientReports(
readBool(metadata, logger, CLIENT_REPORTS_ENABLE, options.isSendClientReports()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,46 @@
import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.sentry.Attachment;
import io.sentry.EventProcessor;
import io.sentry.Hint;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
import java.io.Closeable;
import java.io.IOException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* ScreenshotEventProcessor responsible for taking a screenshot of the screen when an error is
* captured.
*/
@ApiStatus.Internal
public final class ScreenshotEventProcessor
implements EventProcessor, Application.ActivityLifecycleCallbacks, Closeable {
public final class ScreenshotEventProcessor implements EventProcessor {

private final @NotNull Application application;
private final @NotNull SentryAndroidOptions options;
private final @NotNull BuildInfoProvider buildInfoProvider;
private boolean lifecycleCallbackInstalled = true;

public ScreenshotEventProcessor(
final @NotNull Application application,
final @NotNull SentryAndroidOptions options,
final @NotNull BuildInfoProvider buildInfoProvider) {
this.application = Objects.requireNonNull(application, "Application is required");
this.options = Objects.requireNonNull(options, "SentryAndroidOptions is required");
this.buildInfoProvider =
Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required");

application.registerActivityLifecycleCallbacks(this);
}

@SuppressWarnings("NullAway")
@Override
public @NotNull SentryEvent process(final @NotNull SentryEvent event, @NotNull Hint hint) {
if (!lifecycleCallbackInstalled || !event.isErrored()) {
public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) {
if (!event.isErrored()) {
return event;
}
if (!options.isAttachScreenshot()) {
application.unregisterActivityLifecycleCallbacks(this);
lifecycleCallbackInstalled = false;

this.options
.getLogger()
.log(
SentryLevel.DEBUG,
"attachScreenshot is disabled, ScreenshotEventProcessor isn't installed.");
this.options.getLogger().log(SentryLevel.DEBUG, "attachScreenshot is disabled.");

return event;
}
final Activity activity = CurrentActivityHolder.getInstance().getActivity();
final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
if (activity == null || HintUtils.isFromHybridSdk(hint)) {
return event;
}
Expand All @@ -77,55 +57,4 @@ public ScreenshotEventProcessor(
hint.set(ANDROID_ACTIVITY, activity);
return event;
}

@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
CurrentActivityHolder.getInstance().setActivity(activity);
}

@Override
public void onActivityStarted(@NonNull Activity activity) {
setCurrentActivity(activity);
}

@Override
public void onActivityResumed(@NonNull Activity activity) {
setCurrentActivity(activity);
}

@Override
public void onActivityPaused(@NonNull Activity activity) {
cleanCurrentActivity(activity);
}

@Override
public void onActivityStopped(@NonNull Activity activity) {
cleanCurrentActivity(activity);
}

@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}

@Override
public void onActivityDestroyed(@NonNull Activity activity) {
cleanCurrentActivity(activity);
}

@Override
public void close() throws IOException {
if (options.isAttachScreenshot()) {
application.unregisterActivityLifecycleCallbacks(this);
CurrentActivityHolder.getInstance().clearActivity();
}
}

private void cleanCurrentActivity(@NonNull Activity activity) {
if (CurrentActivityHolder.getInstance().getActivity() == activity) {
CurrentActivityHolder.getInstance().clearActivity();
}
}

private void setCurrentActivity(@NonNull Activity activity) {
CurrentActivityHolder.getInstance().setActivity(activity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public final class SentryAndroidOptions extends SentryOptions {
/** Enables or disables the attach screenshot feature when an error happened. */
private boolean attachScreenshot;

/** Enables or disables the attach view hierarchy feature when an error happened. */
private boolean attachViewHierarchy;

/**
* Enables or disables collecting of device information which requires Inter-Process Communication
* (IPC)
Expand Down Expand Up @@ -329,6 +332,14 @@ public void setAttachScreenshot(boolean attachScreenshot) {
this.attachScreenshot = attachScreenshot;
}

public boolean isAttachViewHierarchy() {
return attachViewHierarchy;
}

public void setAttachViewHierarchy(boolean attachViewHierarchy) {
this.attachViewHierarchy = attachViewHierarchy;
}

public boolean isCollectAdditionalContext() {
return collectAdditionalContext;
}
Expand Down
Loading