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

Make possible screenshots on hybrid sdks (react-native) #2360

Merged
merged 33 commits into from Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fc20d26
Fix do not add screenshots if should not apply scope data, make setCu…
krystofwoldrich Nov 14, 2022
d9a2e69
Format code
getsentry-bot Nov 14, 2022
6615b75
Expose ScreenshotEventProcessor to allow HybridSDKs to use it
krystofwoldrich Nov 14, 2022
0dddd51
Merge remote-tracking branch 'origin/feat-screenshots-for-hybrid-sdks…
krystofwoldrich Nov 15, 2022
d7b8109
Revert gradle.properties
krystofwoldrich Nov 15, 2022
33eb7e2
Format code
getsentry-bot Nov 15, 2022
98459b9
Return correctly when invalid activity
krystofwoldrich Nov 15, 2022
ba66365
Update api
krystofwoldrich Nov 15, 2022
465bbf3
Extract screenshot logic to static method, create activity holder sin…
krystofwoldrich Nov 16, 2022
97c3233
Merge branch 'main' into feat-screenshots-for-hybrid-sdks
krystofwoldrich Nov 16, 2022
706dc8e
Format code
getsentry-bot Nov 16, 2022
8c89da7
Refactor
krystofwoldrich Nov 16, 2022
2cce844
Make logger public
krystofwoldrich Nov 16, 2022
66b4e9e
Add configurable tag to android logger
krystofwoldrich Nov 16, 2022
fde7981
Format code
getsentry-bot Nov 16, 2022
ec05044
Update sentry-android-core/src/main/java/io/sentry/android/core/Andro…
krystofwoldrich Nov 16, 2022
2e19e51
Fix activity holder access, return build info
krystofwoldrich Nov 16, 2022
42e606b
Format code
getsentry-bot Nov 16, 2022
5aee06d
Merge branch 'main' into feat-screenshots-for-hybrid-sdks
krystofwoldrich Nov 16, 2022
0299512
Fix lint
krystofwoldrich Nov 16, 2022
6d57ab3
Merge branch 'main' into feat-screenshots-for-hybrid-sdks
krystofwoldrich Nov 16, 2022
03e03b9
Apply suggestions from code review
krystofwoldrich Nov 17, 2022
47e66be
Small refactor and add more hybrid sdks
krystofwoldrich Nov 17, 2022
ce5d2ce
Format code
getsentry-bot Nov 17, 2022
867a810
More refactoring annotations, finals
krystofwoldrich Nov 17, 2022
3e9008d
Add auto close steam
krystofwoldrich Nov 17, 2022
ce00db0
Merge branch 'main' into feat-screenshots-for-hybrid-sdks
krystofwoldrich Nov 21, 2022
0ce3f0e
HintUtils add only class annotation internal
krystofwoldrich Nov 21, 2022
baa0a64
CurrentActivityHolder make constructor private
krystofwoldrich Nov 21, 2022
8ebcaa2
Add changelog (don't attach screenshots to hybridSDKs)
krystofwoldrich Nov 21, 2022
8b49d10
Format code
getsentry-bot Nov 21, 2022
3bb055f
Update changelog
krystofwoldrich Nov 21, 2022
6a469aa
Revert "Update changelog"
krystofwoldrich Nov 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Expand Up @@ -22,6 +22,15 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/core/AndroidLogger : io/sentry/ILogger {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
public fun isEnabled (Lio/sentry/SentryLevel;)Z
public fun log (Lio/sentry/SentryLevel;Ljava/lang/String;Ljava/lang/Throwable;)V
public fun log (Lio/sentry/SentryLevel;Ljava/lang/String;[Ljava/lang/Object;)V
public fun log (Lio/sentry/SentryLevel;Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
}

public final class io/sentry/android/core/AnrIntegration : io/sentry/Integration, java/io/Closeable {
public fun <init> (Landroid/content/Context;)V
public fun close ()V
Expand Down Expand Up @@ -72,6 +81,14 @@ public final class io/sentry/android/core/BuildInfoProvider {
public fun isEmulator ()Ljava/lang/Boolean;
}

public class io/sentry/android/core/CurrentActivityHolder {
public fun <init> ()V
public fun clearActivity ()V
public fun getActivity ()Landroid/app/Activity;
public static fun getInstance ()Lio/sentry/android/core/CurrentActivityHolder;
public fun setActivity (Landroid/app/Activity;)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 @@ -3,12 +3,22 @@
import android.util.Log;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

final class AndroidLogger implements ILogger {
@ApiStatus.Internal
public final class AndroidLogger implements ILogger {

private static final String tag = "Sentry";
private final String tag;
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

public AndroidLogger() {
this("Sentry");
}

public AndroidLogger(@NotNull String tag) {
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
this.tag = tag;
}

@SuppressWarnings("AnnotateFormatMethod")
@Override
Expand Down
@@ -0,0 +1,39 @@
package io.sentry.android.core;

import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

@ApiStatus.Internal
public class CurrentActivityHolder {

private static @NotNull CurrentActivityHolder instance = new CurrentActivityHolder();

krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
private @Nullable WeakReference<Activity> currentActivity;

public static CurrentActivityHolder getInstance() {
return instance;
}

public @Nullable Activity getActivity() {
if (currentActivity != null) {
return currentActivity.get();
}
return null;
}

public void setActivity(@NonNull Activity activity) {
if (currentActivity != null && currentActivity.get() == activity) {
return;
}

currentActivity = new WeakReference<>(activity);
}

public void clearActivity() {
currentActivity = null;
}
}
@@ -1,27 +1,22 @@
package io.sentry.android.core;

import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY;
import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;

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.Hint;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

Expand All @@ -35,7 +30,6 @@ public final class ScreenshotEventProcessor

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

Expand All @@ -54,7 +48,7 @@ public ScreenshotEventProcessor(
@SuppressWarnings("NullAway")
@Override
public @NotNull SentryEvent process(final @NotNull SentryEvent event, @NotNull Hint hint) {
if (!lifecycleCallbackInstalled) {
if (!lifecycleCallbackInstalled || !event.isErrored()) {
return event;
}
if (!options.isAttachScreenshot()) {
Expand All @@ -69,60 +63,28 @@ public ScreenshotEventProcessor(

return event;
}
if (CurrentActivityHolder.getInstance().getActivity() == null
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
|| HintUtils.isFromHybridSdk(hint)) {
return event;
}

if (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 (byteArrayOutputStream.size() > 0) {
// screenshot png is around ~100-150 kb
hint.setScreenshot(Attachment.fromScreenshot(byteArrayOutputStream.toByteArray()));
hint.set(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.");
}
final byte[] screenshot =
takeScreenshot(
CurrentActivityHolder.getInstance().getActivity(),
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
options.getLogger(),
buildInfoProvider);
if (screenshot == null) {
return event;
}

hint.setScreenshot(Attachment.fromScreenshot(screenshot));
hint.set(ANDROID_ACTIVITY, CurrentActivityHolder.getInstance().getActivity());
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
return event;
}

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

@Override
Expand Down Expand Up @@ -157,32 +119,17 @@ public void onActivityDestroyed(@NonNull Activity activity) {
public void close() throws IOException {
if (options.isAttachScreenshot()) {
application.unregisterActivityLifecycleCallbacks(this);
currentActivity = null;
CurrentActivityHolder.getInstance().clearActivity();
}
}

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

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();
}
CurrentActivityHolder.getInstance().setActivity(activity);
}
}
@@ -0,0 +1,80 @@
package io.sentry.android.core.internal.util;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Build;
import android.view.View;
import androidx.annotation.Nullable;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import io.sentry.android.core.BuildInfoProvider;
import java.io.ByteArrayOutputStream;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

@ApiStatus.Internal
public class ScreenshotUtils {
public static @Nullable byte[] takeScreenshot(
final @Nullable Activity activity,
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
final @NotNull ILogger logger,
final @NotNull BuildInfoProvider buildInfoProvider) {
if (activity == null) {
return null;
}

if (!isActivityValid(activity, buildInfoProvider)
|| activity.getWindow() == null
|| activity.getWindow().getDecorView() == null
|| activity.getWindow().getDecorView().getRootView() == null) {
logger.log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot.");
return null;
}

final View view = activity.getWindow().getDecorView().getRootView();
if (view.getWidth() <= 0 || view.getHeight() <= 0) {
logger.log(SentryLevel.DEBUG, "View's width and height is zeroed, not taking screenshot.");
return null;
}

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();
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

// 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 (byteArrayOutputStream.size() <= 0) {
logger.log(SentryLevel.DEBUG, "Screenshot is 0 bytes, not attaching the image.");
return null;
}

// screenshot png is around ~100-150 kb
return byteArrayOutputStream.toByteArray();
} catch (Throwable e) {
logger.log(SentryLevel.ERROR, "Taking screenshot failed.", e);
}
return null;
}

@SuppressLint("NewApi")
public static boolean isActivityValid(
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
final @Nullable Activity activity, final @NotNull BuildInfoProvider buildInfoProvider) {
if (activity == null) {
return false;
}
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return !activity.isFinishing() && !activity.isDestroyed();
} else {
return !activity.isFinishing();
}
}
}
6 changes: 6 additions & 0 deletions sentry/api/sentry.api
Expand Up @@ -1897,6 +1897,9 @@ public final class io/sentry/TypeCheckHint {
public static final field OKHTTP_RESPONSE Ljava/lang/String;
public static final field OPEN_FEIGN_REQUEST Ljava/lang/String;
public static final field OPEN_FEIGN_RESPONSE Ljava/lang/String;
public static final field SENTRY_DART_SDK_NAME Ljava/lang/String;
public static final field SENTRY_IS_FROM_HYBRID_SDK Ljava/lang/String;
public static final field SENTRY_REACT_NATIVE_SDK_NAME Ljava/lang/String;
public static final field SENTRY_SYNTHETIC_EXCEPTION Ljava/lang/String;
public static final field SENTRY_TYPE_CHECK_HINT Ljava/lang/String;
public static final field SERVLET_REQUEST Ljava/lang/String;
Expand Down Expand Up @@ -3303,10 +3306,13 @@ public final class io/sentry/util/HintUtils {
public static fun createWithTypeCheckHint (Ljava/lang/Object;)Lio/sentry/Hint;
public static fun getSentrySdkHint (Lio/sentry/Hint;)Ljava/lang/Object;
public static fun hasType (Lio/sentry/Hint;Ljava/lang/Class;)Z
public static fun isFromHybridSdk (Lio/sentry/Hint;)Z
public static fun runIfDoesNotHaveType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryNullableConsumer;)V
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;)V
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;Lio/sentry/util/HintUtils$SentryHintFallback;)V
public static fun runIfHasTypeLogIfNot (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/ILogger;Lio/sentry/util/HintUtils$SentryConsumer;)V
public static fun setIsFromHybridSdk (Lio/sentry/Hint;Ljava/lang/String;)V
public static fun setIsFromHybridSdk (Lio/sentry/Hint;Z)V
public static fun setTypeCheckHint (Lio/sentry/Hint;Ljava/lang/Object;)V
public static fun shouldApplyScopeData (Lio/sentry/Hint;)Z
}
Expand Down
3 changes: 3 additions & 0 deletions sentry/src/main/java/io/sentry/OutboxSender.java
Expand Up @@ -133,6 +133,9 @@ private void processEnvelope(final @NotNull SentryEnvelope envelope, final @NotN
if (event == null) {
logEnvelopeItemNull(item, currentItem);
} else {
if (event.getSdk() != null) {
HintUtils.setIsFromHybridSdk(hint, event.getSdk().getName());
}
if (envelope.getHeader().getEventId() != null
&& !envelope.getHeader().getEventId().equals(event.getEventId())) {
logUnexpectedEventId(envelope, event.getEventId(), currentItem);
Expand Down
8 changes: 8 additions & 0 deletions sentry/src/main/java/io/sentry/TypeCheckHint.java
Expand Up @@ -7,6 +7,14 @@ public final class TypeCheckHint {

@ApiStatus.Internal public static final String SENTRY_TYPE_CHECK_HINT = "sentry:typeCheckHint";

@ApiStatus.Internal
public static final String SENTRY_IS_FROM_HYBRID_SDK = "sentry:isFromHybridSdk";

@ApiStatus.Internal
public static final String SENTRY_REACT_NATIVE_SDK_NAME = "sentry.javascript.react-native";

@ApiStatus.Internal public static final String SENTRY_DART_SDK_NAME = "sentry.dart";

/** Used for Synthetic exceptions. */
public static final String SENTRY_SYNTHETIC_EXCEPTION = "syntheticException";

Expand Down