Skip to content

Commit

Permalink
Feat: screenshots (#1967)
Browse files Browse the repository at this point in the history
Co-authored-by: Sentry Github Bot <[email protected]>
  • Loading branch information
marandaneto and getsentry-bot authored Apr 6, 2022
1 parent 1e2a01e commit 4414ba0
Show file tree
Hide file tree
Showing 18 changed files with 586 additions and 30 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* Feat: Screenshot is taken when there is an error (#1967)

## 6.0.0-alpha.4

* Ref: Remove not needed interface abstractions on Android (#1953)
Expand Down
15 changes: 15 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ 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 fun process (Lio/sentry/SentryEvent;Ljava/util/Map;)Lio/sentry/SentryEvent;
}

public final class io/sentry/android/core/SentryAndroid {
public static fun init (Landroid/content/Context;)V
public static fun init (Landroid/content/Context;Lio/sentry/ILogger;)V
Expand All @@ -110,6 +123,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader;
public fun isAnrEnabled ()Z
public fun isAnrReportInDebug ()Z
public fun isAttachScreenshot ()Z
public fun isEnableActivityLifecycleBreadcrumbs ()Z
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
public fun isEnableAppComponentBreadcrumbs ()Z
Expand All @@ -120,6 +134,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setAnrEnabled (Z)V
public fun setAnrReportInDebug (Z)V
public fun setAnrTimeoutIntervalMillis (J)V
public fun setAttachScreenshot (Z)V
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import io.sentry.SendFireAndForgetEnvelopeSender;
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;
Expand All @@ -27,7 +26,7 @@

/**
* Android Options initializer, it reads configurations from AndroidManifest and sets to the
* SentryOptions. It also adds default values for some fields.
* SentryAndroidOptions. It also adds default values for some fields.
*/
@SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references
final class AndroidOptionsInitializer {
Expand All @@ -38,7 +37,7 @@ private AndroidOptionsInitializer() {}
/**
* Init method of the Android Options initializer
*
* @param options the SentryOptions
* @param options the SentryAndroidOptions
* @param context the Application context
*/
static void init(final @NotNull SentryAndroidOptions options, final @NotNull Context context) {
Expand All @@ -51,7 +50,7 @@ static void init(final @NotNull SentryAndroidOptions options, final @NotNull Con
/**
* Init method of the Android Options initializer
*
* @param options the SentryOptions
* @param options the SentryAndroidOptions
* @param context the Application context
* @param logger the ILogger interface
* @param isFragmentAvailable whether the Fragment integration is available on the classpath
Expand All @@ -69,7 +68,7 @@ static void init(
/**
* Init method of the Android Options initializer
*
* @param options the SentryOptions
* @param options the SentryAndroidOptions
* @param context the Application context
* @param logger the ILogger interface
* @param buildInfoProvider the BuildInfoProvider interface
Expand All @@ -96,7 +95,7 @@ static void init(
/**
* Init method of the Android Options initializer
*
* @param options the SentryOptions
* @param options the SentryAndroidOptions
* @param context the Application context
* @param logger the ILogger interface
* @param buildInfoProvider the BuildInfoProvider interface
Expand Down Expand Up @@ -149,7 +148,7 @@ static void init(

private static void installDefaultIntegrations(
final @NotNull Context context,
final @NotNull SentryOptions options,
final @NotNull SentryAndroidOptions options,
final @NotNull BuildInfoProvider buildInfoProvider,
final @NotNull LoadClass loadClass,
final @NotNull ActivityFramesTracker activityFramesTracker,
Expand Down Expand Up @@ -191,6 +190,8 @@ private static void installDefaultIntegrations(
if (isFragmentAvailable) {
options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true));
}
options.addEventProcessor(
new ScreenshotEventProcessor((Application) context, options, buildInfoProvider));
} else {
options
.getLogger()
Expand All @@ -210,7 +211,7 @@ private static void installDefaultIntegrations(
/**
* Reads and sets default option values that are Android specific like release and inApp
*
* @param options the SentryOptions
* @param options the SentryAndroidOptions
* @param context the Android context methods
*/
private static void readDefaultOptionValues(
Expand Down Expand Up @@ -285,10 +286,10 @@ private static void readDefaultOptionValues(
* Sets the cache dirs like sentry, outbox and sessions
*
* @param context the Application context
* @param options the SentryOptions
* @param options the SentryAndroidOptions
*/
private static void initializeCacheDirs(
final @NotNull Context context, final @NotNull SentryOptions options) {
final @NotNull Context context, final @NotNull SentryAndroidOptions options) {
final File cacheDir = new File(context.getCacheDir(), "sentry");
options.setCacheDirPath(cacheDir.getAbsolutePath());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ final class ManifestMetadataReader {
static final String ATTACH_THREADS = "io.sentry.attach-threads";
static final String PROGUARD_UUID = "io.sentry.proguard-uuid";

static final String ATTACH_SCREENSHOT = "io.sentry.attach-screenshot";

/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}

Expand Down Expand Up @@ -194,6 +196,9 @@ static void applyMetadata(
options.setAttachThreads(
readBool(metadata, logger, ATTACH_THREADS, options.isAttachThreads()));

options.setAttachScreenshot(
readBool(metadata, logger, ATTACH_SCREENSHOT, options.isAttachScreenshot()));

if (options.getTracesSampleRate() == null) {
final Double tracesSampleRate = readDouble(metadata, logger, TRACES_SAMPLE_RATE);
if (tracesSampleRate != -1) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package io.sentry.android.core;

import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY;
import static io.sentry.TypeCheckHint.SENTRY_SCREENSHOT;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.sentry.Attachment;
import io.sentry.EventProcessor;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.util.Objects;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* 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 {

private final @NotNull Application application;
private final @NotNull SentryAndroidOptions options;
private @Nullable WeakReference<Activity> currentActivity;
final @NotNull BuildInfoProvider buildInfoProvider;

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");

if (this.options.isAttachScreenshot()) {
application.registerActivityLifecycleCallbacks(this);

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

@SuppressWarnings("NullAway")
@Override
public @NotNull SentryEvent process(
final @NotNull SentryEvent event, @Nullable Map<String, Object> hint) {
if (options.isAttachScreenshot() && event.isErrored() && currentActivity != null) {
final Activity activity = currentActivity.get();
if (isActivityValid(activity)
&& activity.getWindow() != null
&& activity.getWindow().getDecorView() != null
&& activity.getWindow().getDecorView().getRootView() != null) {
final View view = activity.getWindow().getDecorView().getRootView();

if (view.getWidth() > 0 && view.getHeight() > 0) {
try {
// ARGB_8888 -> This configuration is very flexible and offers the best quality
final Bitmap bitmap =
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);

final Canvas canvas = new Canvas(bitmap);
view.draw(canvas);

final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

// 0 meaning compress for small size, 100 meaning compress for max quality.
// Some formats, like PNG which is lossless, will ignore the quality setting.
bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);

if (hint == null) {
hint = new HashMap<>();
}

if (byteArrayOutputStream.size() > 0) {
// screenshot png is around ~100-150 kb
hint.put(
SENTRY_SCREENSHOT,
Attachment.fromScreenshot(byteArrayOutputStream.toByteArray()));
hint.put(ANDROID_ACTIVITY, activity);
} else {
this.options
.getLogger()
.log(SentryLevel.DEBUG, "Screenshot is 0 bytes, not attaching the image.");
}
} catch (Throwable e) {
this.options.getLogger().log(SentryLevel.ERROR, "Taking screenshot failed.", e);
}
} else {
this.options
.getLogger()
.log(SentryLevel.DEBUG, "View's width and height is zeroed, not taking screenshot.");
}
} else {
this.options
.getLogger()
.log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot.");
}
}

return event;
}

@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 {
if (options.isAttachScreenshot()) {
application.unregisterActivityLifecycleCallbacks(this);
currentActivity = null;
}
}

private void cleanCurrentActivity(@NonNull Activity activity) {
if (currentActivity != null && currentActivity.get() == activity) {
currentActivity = null;
}
}

private void setCurrentActivity(@NonNull Activity activity) {
if (currentActivity != null && currentActivity.get() == activity) {
return;
}
currentActivity = new WeakReference<>(activity);
}

@SuppressLint("NewApi")
private boolean isActivityValid(@Nullable Activity activity) {
if (activity == null) {
return false;
}
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return !activity.isFinishing() && !activity.isDestroyed();
} else {
return !activity.isFinishing();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ public final class SentryAndroidOptions extends SentryOptions {
/** Interface that loads the debug images list */
private @NotNull IDebugImagesLoader debugImagesLoader = NoOpDebugImagesLoader.getInstance();

/** Enables or disables the attach screenshot feature when an error happened. */
private boolean attachScreenshot;

public SentryAndroidOptions() {
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
setSdkVersion(createSdkVersion());
Expand Down Expand Up @@ -251,4 +254,12 @@ public void setEnableActivityLifecycleTracingAutoFinish(
boolean enableActivityLifecycleTracingAutoFinish) {
this.enableActivityLifecycleTracingAutoFinish = enableActivityLifecycleTracingAutoFinish;
}

public boolean isAttachScreenshot() {
return attachScreenshot;
}

public void setAttachScreenshot(boolean attachScreenshot) {
this.attachScreenshot = attachScreenshot;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ class AndroidOptionsInitializerTest {
assertNotNull(actual)
}

@Test
fun `ScreenshotEventProcessor added to processors list`() {
fixture.initSut()
val actual =
fixture.sentryOptions.eventProcessors.any { it is ScreenshotEventProcessor }
assertNotNull(actual)
}

@Test
fun `envelopesDir should be set at initialization`() {
fixture.initSut()
Expand Down
Loading

0 comments on commit 4414ba0

Please sign in to comment.