Skip to content

Commit 16371c5

Browse files
chore(screenshots): Enable screenshots for Hybrid SDKs (#2360)
2 parents d49f98e + 6a469aa commit 16371c5

File tree

10 files changed

+192
-85
lines changed

10 files changed

+192
-85
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Use `canonicalName` in Fragment Integration for better de-obfuscation ([#2379](https://github.com/getsentry/sentry-java/pull/2379))
88
- Fix Timber and Fragment integrations auto-installation for obfuscated builds ([#2379](https://github.com/getsentry/sentry-java/pull/2379))
9+
- Don't attach screenshots to events from Hybrid SDKs ([#2360](https://github.com/getsentry/sentry-java/pull/2360))
910

1011
## 6.8.0
1112

sentry-android-core/api/sentry-android-core.api

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
2222
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
2323
}
2424

25+
public final class io/sentry/android/core/AndroidLogger : io/sentry/ILogger {
26+
public fun <init> ()V
27+
public fun <init> (Ljava/lang/String;)V
28+
public fun isEnabled (Lio/sentry/SentryLevel;)Z
29+
public fun log (Lio/sentry/SentryLevel;Ljava/lang/String;Ljava/lang/Throwable;)V
30+
public fun log (Lio/sentry/SentryLevel;Ljava/lang/String;[Ljava/lang/Object;)V
31+
public fun log (Lio/sentry/SentryLevel;Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
32+
}
33+
2534
public final class io/sentry/android/core/AnrIntegration : io/sentry/Integration, java/io/Closeable {
2635
public fun <init> (Landroid/content/Context;)V
2736
public fun close ()V
@@ -72,6 +81,13 @@ public final class io/sentry/android/core/BuildInfoProvider {
7281
public fun isEmulator ()Ljava/lang/Boolean;
7382
}
7483

84+
public class io/sentry/android/core/CurrentActivityHolder {
85+
public fun clearActivity ()V
86+
public fun getActivity ()Landroid/app/Activity;
87+
public static fun getInstance ()Lio/sentry/android/core/CurrentActivityHolder;
88+
public fun setActivity (Landroid/app/Activity;)V
89+
}
90+
7591
public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : io/sentry/Integration, java/io/Closeable {
7692
public fun <init> ()V
7793
public fun close ()V

sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@
33
import android.util.Log;
44
import io.sentry.ILogger;
55
import io.sentry.SentryLevel;
6+
import org.jetbrains.annotations.ApiStatus;
67
import org.jetbrains.annotations.NotNull;
78
import org.jetbrains.annotations.Nullable;
89

9-
final class AndroidLogger implements ILogger {
10+
@ApiStatus.Internal
11+
public final class AndroidLogger implements ILogger {
1012

11-
private static final String tag = "Sentry";
13+
private final @NotNull String tag;
14+
15+
public AndroidLogger() {
16+
this("Sentry");
17+
}
18+
19+
public AndroidLogger(final @NotNull String tag) {
20+
this.tag = tag;
21+
}
1222

1323
@SuppressWarnings("AnnotateFormatMethod")
1424
@Override
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.sentry.android.core;
2+
3+
import android.app.Activity;
4+
import androidx.annotation.NonNull;
5+
import androidx.annotation.Nullable;
6+
import java.lang.ref.WeakReference;
7+
import org.jetbrains.annotations.ApiStatus;
8+
import org.jetbrains.annotations.NotNull;
9+
10+
@ApiStatus.Internal
11+
public class CurrentActivityHolder {
12+
13+
private static final @NotNull CurrentActivityHolder instance = new CurrentActivityHolder();
14+
15+
private CurrentActivityHolder() {}
16+
17+
private @Nullable WeakReference<Activity> currentActivity;
18+
19+
public static @NonNull CurrentActivityHolder getInstance() {
20+
return instance;
21+
}
22+
23+
public @Nullable Activity getActivity() {
24+
if (currentActivity != null) {
25+
return currentActivity.get();
26+
}
27+
return null;
28+
}
29+
30+
public void setActivity(final @NonNull Activity activity) {
31+
if (currentActivity != null && currentActivity.get() == activity) {
32+
return;
33+
}
34+
35+
currentActivity = new WeakReference<>(activity);
36+
}
37+
38+
public void clearActivity() {
39+
currentActivity = null;
40+
}
41+
}
Lines changed: 17 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
11
package io.sentry.android.core;
22

33
import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY;
4+
import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;
45

5-
import android.annotation.SuppressLint;
66
import android.app.Activity;
77
import android.app.Application;
8-
import android.graphics.Bitmap;
9-
import android.graphics.Canvas;
10-
import android.os.Build;
118
import android.os.Bundle;
12-
import android.view.View;
139
import androidx.annotation.NonNull;
1410
import androidx.annotation.Nullable;
1511
import io.sentry.Attachment;
1612
import io.sentry.EventProcessor;
1713
import io.sentry.Hint;
1814
import io.sentry.SentryEvent;
1915
import io.sentry.SentryLevel;
16+
import io.sentry.util.HintUtils;
2017
import io.sentry.util.Objects;
21-
import java.io.ByteArrayOutputStream;
2218
import java.io.Closeable;
2319
import java.io.IOException;
24-
import java.lang.ref.WeakReference;
2520
import org.jetbrains.annotations.ApiStatus;
2621
import org.jetbrains.annotations.NotNull;
2722

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

3631
private final @NotNull Application application;
3732
private final @NotNull SentryAndroidOptions options;
38-
private @Nullable WeakReference<Activity> currentActivity;
3933
private final @NotNull BuildInfoProvider buildInfoProvider;
4034
private boolean lifecycleCallbackInstalled = true;
4135

@@ -54,7 +48,7 @@ public ScreenshotEventProcessor(
5448
@SuppressWarnings("NullAway")
5549
@Override
5650
public @NotNull SentryEvent process(final @NotNull SentryEvent event, @NotNull Hint hint) {
57-
if (!lifecycleCallbackInstalled) {
51+
if (!lifecycleCallbackInstalled || !event.isErrored()) {
5852
return event;
5953
}
6054
if (!options.isAttachScreenshot()) {
@@ -69,60 +63,24 @@ public ScreenshotEventProcessor(
6963

7064
return event;
7165
}
66+
final Activity activity = CurrentActivityHolder.getInstance().getActivity();
67+
if (activity == null || HintUtils.isFromHybridSdk(hint)) {
68+
return event;
69+
}
7270

73-
if (event.isErrored() && currentActivity != null) {
74-
final Activity activity = currentActivity.get();
75-
if (isActivityValid(activity)
76-
&& activity.getWindow() != null
77-
&& activity.getWindow().getDecorView() != null
78-
&& activity.getWindow().getDecorView().getRootView() != null) {
79-
final View view = activity.getWindow().getDecorView().getRootView();
80-
81-
if (view.getWidth() > 0 && view.getHeight() > 0) {
82-
try {
83-
// ARGB_8888 -> This configuration is very flexible and offers the best quality
84-
final Bitmap bitmap =
85-
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
86-
87-
final Canvas canvas = new Canvas(bitmap);
88-
view.draw(canvas);
89-
90-
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
91-
92-
// 0 meaning compress for small size, 100 meaning compress for max quality.
93-
// Some formats, like PNG which is lossless, will ignore the quality setting.
94-
bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
95-
96-
if (byteArrayOutputStream.size() > 0) {
97-
// screenshot png is around ~100-150 kb
98-
hint.setScreenshot(Attachment.fromScreenshot(byteArrayOutputStream.toByteArray()));
99-
hint.set(ANDROID_ACTIVITY, activity);
100-
} else {
101-
this.options
102-
.getLogger()
103-
.log(SentryLevel.DEBUG, "Screenshot is 0 bytes, not attaching the image.");
104-
}
105-
} catch (Throwable e) {
106-
this.options.getLogger().log(SentryLevel.ERROR, "Taking screenshot failed.", e);
107-
}
108-
} else {
109-
this.options
110-
.getLogger()
111-
.log(SentryLevel.DEBUG, "View's width and height is zeroed, not taking screenshot.");
112-
}
113-
} else {
114-
this.options
115-
.getLogger()
116-
.log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot.");
117-
}
71+
final byte[] screenshot = takeScreenshot(activity, options.getLogger(), buildInfoProvider);
72+
if (screenshot == null) {
73+
return event;
11874
}
11975

76+
hint.setScreenshot(Attachment.fromScreenshot(screenshot));
77+
hint.set(ANDROID_ACTIVITY, activity);
12078
return event;
12179
}
12280

12381
@Override
12482
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
125-
setCurrentActivity(activity);
83+
CurrentActivityHolder.getInstance().setActivity(activity);
12684
}
12785

12886
@Override
@@ -157,32 +115,17 @@ public void onActivityDestroyed(@NonNull Activity activity) {
157115
public void close() throws IOException {
158116
if (options.isAttachScreenshot()) {
159117
application.unregisterActivityLifecycleCallbacks(this);
160-
currentActivity = null;
118+
CurrentActivityHolder.getInstance().clearActivity();
161119
}
162120
}
163121

164122
private void cleanCurrentActivity(@NonNull Activity activity) {
165-
if (currentActivity != null && currentActivity.get() == activity) {
166-
currentActivity = null;
123+
if (CurrentActivityHolder.getInstance().getActivity() == activity) {
124+
CurrentActivityHolder.getInstance().clearActivity();
167125
}
168126
}
169127

170128
private void setCurrentActivity(@NonNull Activity activity) {
171-
if (currentActivity != null && currentActivity.get() == activity) {
172-
return;
173-
}
174-
currentActivity = new WeakReference<>(activity);
175-
}
176-
177-
@SuppressLint("NewApi")
178-
private boolean isActivityValid(@Nullable Activity activity) {
179-
if (activity == null) {
180-
return false;
181-
}
182-
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
183-
return !activity.isFinishing() && !activity.isDestroyed();
184-
} else {
185-
return !activity.isFinishing();
186-
}
129+
CurrentActivityHolder.getInstance().setActivity(activity);
187130
}
188131
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.sentry.android.core.internal.util;
2+
3+
import android.annotation.SuppressLint;
4+
import android.app.Activity;
5+
import android.graphics.Bitmap;
6+
import android.graphics.Canvas;
7+
import android.os.Build;
8+
import android.view.View;
9+
import androidx.annotation.Nullable;
10+
import io.sentry.ILogger;
11+
import io.sentry.SentryLevel;
12+
import io.sentry.android.core.BuildInfoProvider;
13+
import java.io.ByteArrayOutputStream;
14+
import org.jetbrains.annotations.ApiStatus;
15+
import org.jetbrains.annotations.NotNull;
16+
17+
@ApiStatus.Internal
18+
public class ScreenshotUtils {
19+
public static @Nullable byte[] takeScreenshot(
20+
final @NotNull Activity activity,
21+
final @NotNull ILogger logger,
22+
final @NotNull BuildInfoProvider buildInfoProvider) {
23+
if (!isActivityValid(activity, buildInfoProvider)
24+
|| activity.getWindow() == null
25+
|| activity.getWindow().getDecorView() == null
26+
|| activity.getWindow().getDecorView().getRootView() == null) {
27+
logger.log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot.");
28+
return null;
29+
}
30+
31+
final View view = activity.getWindow().getDecorView().getRootView();
32+
if (view.getWidth() <= 0 || view.getHeight() <= 0) {
33+
logger.log(SentryLevel.DEBUG, "View's width and height is zeroed, not taking screenshot.");
34+
return null;
35+
}
36+
37+
try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
38+
// ARGB_8888 -> This configuration is very flexible and offers the best quality
39+
final Bitmap bitmap =
40+
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
41+
42+
final Canvas canvas = new Canvas(bitmap);
43+
view.draw(canvas);
44+
45+
// 0 meaning compress for small size, 100 meaning compress for max quality.
46+
// Some formats, like PNG which is lossless, will ignore the quality setting.
47+
bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
48+
49+
if (byteArrayOutputStream.size() <= 0) {
50+
logger.log(SentryLevel.DEBUG, "Screenshot is 0 bytes, not attaching the image.");
51+
return null;
52+
}
53+
54+
// screenshot png is around ~100-150 kb
55+
return byteArrayOutputStream.toByteArray();
56+
} catch (Throwable e) {
57+
logger.log(SentryLevel.ERROR, "Taking screenshot failed.", e);
58+
}
59+
return null;
60+
}
61+
62+
@SuppressLint("NewApi")
63+
private static boolean isActivityValid(
64+
final @NotNull Activity activity, final @NotNull BuildInfoProvider buildInfoProvider) {
65+
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
66+
return !activity.isFinishing() && !activity.isDestroyed();
67+
} else {
68+
return !activity.isFinishing();
69+
}
70+
}
71+
}

sentry/api/sentry.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,6 +1901,10 @@ public final class io/sentry/TypeCheckHint {
19011901
public static final field OKHTTP_RESPONSE Ljava/lang/String;
19021902
public static final field OPEN_FEIGN_REQUEST Ljava/lang/String;
19031903
public static final field OPEN_FEIGN_RESPONSE Ljava/lang/String;
1904+
public static final field SENTRY_DART_SDK_NAME Ljava/lang/String;
1905+
public static final field SENTRY_DOTNET_SDK_NAME Ljava/lang/String;
1906+
public static final field SENTRY_IS_FROM_HYBRID_SDK Ljava/lang/String;
1907+
public static final field SENTRY_JAVASCRIPT_SDK_NAME Ljava/lang/String;
19041908
public static final field SENTRY_SYNTHETIC_EXCEPTION Ljava/lang/String;
19051909
public static final field SENTRY_TYPE_CHECK_HINT Ljava/lang/String;
19061910
public static final field SERVLET_REQUEST Ljava/lang/String;
@@ -3362,10 +3366,12 @@ public final class io/sentry/util/HintUtils {
33623366
public static fun createWithTypeCheckHint (Ljava/lang/Object;)Lio/sentry/Hint;
33633367
public static fun getSentrySdkHint (Lio/sentry/Hint;)Ljava/lang/Object;
33643368
public static fun hasType (Lio/sentry/Hint;Ljava/lang/Class;)Z
3369+
public static fun isFromHybridSdk (Lio/sentry/Hint;)Z
33653370
public static fun runIfDoesNotHaveType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryNullableConsumer;)V
33663371
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;)V
33673372
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;Lio/sentry/util/HintUtils$SentryHintFallback;)V
33683373
public static fun runIfHasTypeLogIfNot (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/ILogger;Lio/sentry/util/HintUtils$SentryConsumer;)V
3374+
public static fun setIsFromHybridSdk (Lio/sentry/Hint;Ljava/lang/String;)V
33693375
public static fun setTypeCheckHint (Lio/sentry/Hint;Ljava/lang/Object;)V
33703376
public static fun shouldApplyScopeData (Lio/sentry/Hint;)Z
33713377
}

sentry/src/main/java/io/sentry/OutboxSender.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ private void processEnvelope(final @NotNull SentryEnvelope envelope, final @NotN
133133
if (event == null) {
134134
logEnvelopeItemNull(item, currentItem);
135135
} else {
136+
if (event.getSdk() != null) {
137+
HintUtils.setIsFromHybridSdk(hint, event.getSdk().getName());
138+
}
136139
if (envelope.getHeader().getEventId() != null
137140
&& !envelope.getHeader().getEventId().equals(event.getEventId())) {
138141
logUnexpectedEventId(envelope, event.getEventId(), currentItem);

sentry/src/main/java/io/sentry/TypeCheckHint.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ public final class TypeCheckHint {
77

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

10+
@ApiStatus.Internal
11+
public static final String SENTRY_IS_FROM_HYBRID_SDK = "sentry:isFromHybridSdk";
12+
13+
@ApiStatus.Internal public static final String SENTRY_JAVASCRIPT_SDK_NAME = "sentry.javascript";
14+
15+
@ApiStatus.Internal public static final String SENTRY_DOTNET_SDK_NAME = "sentry.dotnet";
16+
17+
@ApiStatus.Internal public static final String SENTRY_DART_SDK_NAME = "sentry.dart";
18+
1019
/** Used for Synthetic exceptions. */
1120
public static final String SENTRY_SYNTHETIC_EXCEPTION = "syntheticException";
1221

0 commit comments

Comments
 (0)