diff --git a/CHANGELOG.md b/CHANGELOG.md index a874b3954d..5e8c62beb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Use `canonicalName` in Fragment Integration for better de-obfuscation ([#2379](https://github.com/getsentry/sentry-java/pull/2379)) - Fix Timber and Fragment integrations auto-installation for obfuscated builds ([#2379](https://github.com/getsentry/sentry-java/pull/2379)) +- Don't attach screenshots to events from Hybrid SDKs ([#2360](https://github.com/getsentry/sentry-java/pull/2360)) ## 6.8.0 diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 2767a3738d..17dca925d3 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -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 ()V + public fun (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 (Landroid/content/Context;)V public fun close ()V @@ -72,6 +81,13 @@ public final class io/sentry/android/core/BuildInfoProvider { public fun isEmulator ()Ljava/lang/Boolean; } +public class io/sentry/android/core/CurrentActivityHolder { + 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 ()V public fun close ()V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java index 5fc1b84af9..b0f6b8ac92 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java @@ -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 @NotNull String tag; + + public AndroidLogger() { + this("Sentry"); + } + + public AndroidLogger(final @NotNull String tag) { + this.tag = tag; + } @SuppressWarnings("AnnotateFormatMethod") @Override diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java b/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java new file mode 100644 index 0000000000..8da322b20b --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java @@ -0,0 +1,41 @@ +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 final @NotNull CurrentActivityHolder instance = new CurrentActivityHolder(); + + private CurrentActivityHolder() {} + + private @Nullable WeakReference currentActivity; + + public static @NonNull CurrentActivityHolder getInstance() { + return instance; + } + + public @Nullable Activity getActivity() { + if (currentActivity != null) { + return currentActivity.get(); + } + return null; + } + + public void setActivity(final @NonNull Activity activity) { + if (currentActivity != null && currentActivity.get() == activity) { + return; + } + + currentActivity = new WeakReference<>(activity); + } + + public void clearActivity() { + currentActivity = null; + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java index 48e99e7ad3..041d441579 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java @@ -1,15 +1,11 @@ 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; @@ -17,11 +13,10 @@ 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; @@ -35,7 +30,6 @@ public final class ScreenshotEventProcessor private final @NotNull Application application; private final @NotNull SentryAndroidOptions options; - private @Nullable WeakReference currentActivity; private final @NotNull BuildInfoProvider buildInfoProvider; private boolean lifecycleCallbackInstalled = true; @@ -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()) { @@ -69,60 +63,24 @@ public ScreenshotEventProcessor( return event; } + final Activity activity = CurrentActivityHolder.getInstance().getActivity(); + if (activity == null || 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(activity, options.getLogger(), buildInfoProvider); + if (screenshot == null) { + return event; } + hint.setScreenshot(Attachment.fromScreenshot(screenshot)); + hint.set(ANDROID_ACTIVITY, activity); return event; } @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { - setCurrentActivity(activity); + CurrentActivityHolder.getInstance().setActivity(activity); } @Override @@ -157,32 +115,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); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java new file mode 100644 index 0000000000..b9a528382e --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java @@ -0,0 +1,71 @@ +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 @NotNull Activity activity, + final @NotNull ILogger logger, + final @NotNull BuildInfoProvider buildInfoProvider) { + 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 (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + // 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); + + // 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") + private static boolean isActivityValid( + final @NotNull Activity activity, final @NotNull BuildInfoProvider buildInfoProvider) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return !activity.isFinishing() && !activity.isDestroyed(); + } else { + return !activity.isFinishing(); + } + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index be010cdc39..d514145178 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1901,6 +1901,10 @@ 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_DOTNET_SDK_NAME Ljava/lang/String; + public static final field SENTRY_IS_FROM_HYBRID_SDK Ljava/lang/String; + public static final field SENTRY_JAVASCRIPT_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; @@ -3362,10 +3366,12 @@ 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 setTypeCheckHint (Lio/sentry/Hint;Ljava/lang/Object;)V public static fun shouldApplyScopeData (Lio/sentry/Hint;)Z } diff --git a/sentry/src/main/java/io/sentry/OutboxSender.java b/sentry/src/main/java/io/sentry/OutboxSender.java index bd2d948316..fcf21fb121 100644 --- a/sentry/src/main/java/io/sentry/OutboxSender.java +++ b/sentry/src/main/java/io/sentry/OutboxSender.java @@ -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); diff --git a/sentry/src/main/java/io/sentry/TypeCheckHint.java b/sentry/src/main/java/io/sentry/TypeCheckHint.java index 11e465ac51..ba2d895ad2 100644 --- a/sentry/src/main/java/io/sentry/TypeCheckHint.java +++ b/sentry/src/main/java/io/sentry/TypeCheckHint.java @@ -7,6 +7,15 @@ 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_JAVASCRIPT_SDK_NAME = "sentry.javascript"; + + @ApiStatus.Internal public static final String SENTRY_DOTNET_SDK_NAME = "sentry.dotnet"; + + @ApiStatus.Internal public static final String SENTRY_DART_SDK_NAME = "sentry.dart"; + /** Used for Synthetic exceptions. */ public static final String SENTRY_SYNTHETIC_EXCEPTION = "syntheticException"; diff --git a/sentry/src/main/java/io/sentry/util/HintUtils.java b/sentry/src/main/java/io/sentry/util/HintUtils.java index 13c6c87d05..fcf752ba56 100644 --- a/sentry/src/main/java/io/sentry/util/HintUtils.java +++ b/sentry/src/main/java/io/sentry/util/HintUtils.java @@ -1,5 +1,9 @@ package io.sentry.util; +import static io.sentry.TypeCheckHint.SENTRY_DART_SDK_NAME; +import static io.sentry.TypeCheckHint.SENTRY_DOTNET_SDK_NAME; +import static io.sentry.TypeCheckHint.SENTRY_IS_FROM_HYBRID_SDK; +import static io.sentry.TypeCheckHint.SENTRY_JAVASCRIPT_SDK_NAME; import static io.sentry.TypeCheckHint.SENTRY_TYPE_CHECK_HINT; import io.sentry.Hint; @@ -16,30 +20,37 @@ public final class HintUtils { private HintUtils() {} - @ApiStatus.Internal + public static void setIsFromHybridSdk(final @NotNull Hint hint, final @NotNull String sdkName) { + if (sdkName.startsWith(SENTRY_JAVASCRIPT_SDK_NAME) + || sdkName.startsWith(SENTRY_DART_SDK_NAME) + || sdkName.startsWith(SENTRY_DOTNET_SDK_NAME)) { + hint.set(SENTRY_IS_FROM_HYBRID_SDK, true); + } + } + + public static boolean isFromHybridSdk(final @NotNull Hint hint) { + return Boolean.TRUE.equals(hint.getAs(SENTRY_IS_FROM_HYBRID_SDK, Boolean.class)); + } + public static Hint createWithTypeCheckHint(Object typeCheckHint) { Hint hint = new Hint(); setTypeCheckHint(hint, typeCheckHint); return hint; } - @ApiStatus.Internal public static void setTypeCheckHint(@NotNull Hint hint, Object typeCheckHint) { hint.set(SENTRY_TYPE_CHECK_HINT, typeCheckHint); } - @ApiStatus.Internal public static @Nullable Object getSentrySdkHint(@NotNull Hint hint) { return hint.get(SENTRY_TYPE_CHECK_HINT); } - @ApiStatus.Internal public static boolean hasType(@NotNull Hint hint, @NotNull Class clazz) { final Object sentrySdkHint = getSentrySdkHint(hint); return clazz.isInstance(sentrySdkHint); } - @ApiStatus.Internal public static void runIfDoesNotHaveType( @NotNull Hint hint, @NotNull Class clazz, SentryNullableConsumer lambda) { runIfHasType( @@ -51,13 +62,11 @@ public static void runIfDoesNotHaveType( }); } - @ApiStatus.Internal public static void runIfHasType( @NotNull Hint hint, @NotNull Class clazz, SentryConsumer lambda) { runIfHasType(hint, clazz, lambda, (value, clazz2) -> {}); } - @ApiStatus.Internal public static void runIfHasTypeLogIfNot( @NotNull Hint hint, @NotNull Class clazz, ILogger logger, SentryConsumer lambda) { runIfHasType( @@ -70,7 +79,6 @@ public static void runIfHasTypeLogIfNot( } @SuppressWarnings("unchecked") - @ApiStatus.Internal public static void runIfHasType( @NotNull Hint hint, @NotNull Class clazz, @@ -90,7 +98,6 @@ public static void runIfHasType( * * @return true if it should apply scope's data or false otherwise */ - @ApiStatus.Internal public static boolean shouldApplyScopeData(@NotNull Hint hint) { return !hasType(hint, Cached.class) || hasType(hint, ApplyScopeData.class); }