Skip to content

Commit 1f78424

Browse files
authored
Merge branch 'main' into feat/continuous-profiling-stop-on-background
2 parents 8283fc0 + f6625b0 commit 1f78424

File tree

9 files changed

+226
-14
lines changed

9 files changed

+226
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
- Continuous Profiling - Add delayed stop ([#4293](https://github.com/getsentry/sentry-java/pull/4293))
99
- Continuous Profiling - Out of Experimental ([#4310](https://github.com/getsentry/sentry-java/pull/4310))
1010

11+
### Fixes
12+
13+
- Compress Screenshots on a background thread ([#4295](https://github.com/getsentry/sentry-java/pull/4295))
14+
1115
## 8.6.0
1216

1317
### Behavioral Changes

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
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;
4+
import static io.sentry.android.core.internal.util.ScreenshotUtils.captureScreenshot;
55
import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
66

77
import android.app.Activity;
8+
import android.graphics.Bitmap;
89
import io.sentry.Attachment;
910
import io.sentry.EventProcessor;
1011
import io.sentry.Hint;
1112
import io.sentry.SentryEvent;
1213
import io.sentry.SentryLevel;
1314
import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
1415
import io.sentry.android.core.internal.util.Debouncer;
16+
import io.sentry.android.core.internal.util.ScreenshotUtils;
1517
import io.sentry.protocol.SentryTransaction;
1618
import io.sentry.util.HintUtils;
1719
import io.sentry.util.Objects;
@@ -87,14 +89,19 @@ public ScreenshotEventProcessor(
8789
return event;
8890
}
8991

90-
final byte[] screenshot =
91-
takeScreenshot(
92+
final Bitmap screenshot =
93+
captureScreenshot(
9294
activity, options.getThreadChecker(), options.getLogger(), buildInfoProvider);
9395
if (screenshot == null) {
9496
return event;
9597
}
9698

97-
hint.setScreenshot(Attachment.fromScreenshot(screenshot));
99+
hint.setScreenshot(
100+
Attachment.fromByteProvider(
101+
() -> ScreenshotUtils.compressBitmapToPng(screenshot, options.getLogger()),
102+
"screenshot.png",
103+
"image/png",
104+
false));
98105
hint.set(ANDROID_ACTIVITY, activity);
99106
return event;
100107
}

sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,44 @@ public class ScreenshotUtils {
2727

2828
private static final long CAPTURE_TIMEOUT_MS = 1000;
2929

30+
// Used by Hybrid SDKs
31+
/**
32+
* @noinspection unused
33+
*/
3034
public static @Nullable byte[] takeScreenshot(
3135
final @NotNull Activity activity,
3236
final @NotNull ILogger logger,
3337
final @NotNull BuildInfoProvider buildInfoProvider) {
3438
return takeScreenshot(activity, AndroidThreadChecker.getInstance(), logger, buildInfoProvider);
3539
}
3640

41+
// Used by Hybrid SDKs
3742
@SuppressLint("NewApi")
3843
public static @Nullable byte[] takeScreenshot(
3944
final @NotNull Activity activity,
4045
final @NotNull IThreadChecker threadChecker,
4146
final @NotNull ILogger logger,
4247
final @NotNull BuildInfoProvider buildInfoProvider) {
48+
49+
final @Nullable Bitmap screenshot =
50+
captureScreenshot(activity, threadChecker, logger, buildInfoProvider);
51+
return compressBitmapToPng(screenshot, logger);
52+
}
53+
54+
public static @Nullable Bitmap captureScreenshot(
55+
final @NotNull Activity activity,
56+
final @NotNull ILogger logger,
57+
final @NotNull BuildInfoProvider buildInfoProvider) {
58+
return captureScreenshot(
59+
activity, AndroidThreadChecker.getInstance(), logger, buildInfoProvider);
60+
}
61+
62+
@SuppressLint("NewApi")
63+
public static @Nullable Bitmap captureScreenshot(
64+
final @NotNull Activity activity,
65+
final @NotNull IThreadChecker threadChecker,
66+
final @NotNull ILogger logger,
67+
final @NotNull BuildInfoProvider buildInfoProvider) {
4368
// We are keeping BuildInfoProvider param for compatibility, as it's being used by
4469
// cross-platform SDKs
4570

@@ -71,7 +96,7 @@ public class ScreenshotUtils {
7196
return null;
7297
}
7398

74-
try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
99+
try {
75100
// ARGB_8888 -> This configuration is very flexible and offers the best quality
76101
final Bitmap bitmap =
77102
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
@@ -132,10 +157,31 @@ public class ScreenshotUtils {
132157
return null;
133158
}
134159
}
160+
return bitmap;
161+
} catch (Throwable e) {
162+
logger.log(SentryLevel.ERROR, "Taking screenshot failed.", e);
163+
}
164+
return null;
165+
}
135166

167+
/**
168+
* Compresses the supplied Bitmap to a PNG byte array. After compression, the Bitmap will be
169+
* recycled.
170+
*
171+
* @param bitmap The bitmap to compress
172+
* @param logger the logger
173+
* @return the Bitmap in PNG format, or null if the bitmap was null, recycled or compressing faile
174+
*/
175+
public static @Nullable byte[] compressBitmapToPng(
176+
final @Nullable Bitmap bitmap, final @NotNull ILogger logger) {
177+
if (bitmap == null || bitmap.isRecycled()) {
178+
return null;
179+
}
180+
try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
136181
// 0 meaning compress for small size, 100 meaning compress for max quality.
137182
// Some formats, like PNG which is lossless, will ignore the quality setting.
138183
bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
184+
bitmap.recycle();
139185

140186
if (byteArrayOutputStream.size() <= 0) {
141187
logger.log(SentryLevel.DEBUG, "Screenshot is 0 bytes, not attaching the image.");
@@ -145,7 +191,7 @@ public class ScreenshotUtils {
145191
// screenshot png is around ~100-150 kb
146192
return byteArrayOutputStream.toByteArray();
147193
} catch (Throwable e) {
148-
logger.log(SentryLevel.ERROR, "Taking screenshot failed.", e);
194+
logger.log(SentryLevel.ERROR, "Compressing bitmap failed.", e);
149195
}
150196
return null;
151197
}

sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilTest.kt

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package io.sentry.android.core.internal.util
22

33
import android.app.Activity
4+
import android.graphics.Bitmap
45
import android.os.Build
56
import android.os.Bundle
67
import android.view.View
78
import android.view.Window
89
import androidx.test.ext.junit.runners.AndroidJUnit4
910
import io.sentry.ILogger
11+
import io.sentry.NoOpLogger
1012
import io.sentry.android.core.BuildInfoProvider
1113
import junit.framework.TestCase.assertNull
1214
import org.junit.runner.RunWith
@@ -16,7 +18,9 @@ import org.robolectric.Robolectric.buildActivity
1618
import org.robolectric.annotation.Config
1719
import org.robolectric.shadows.ShadowPixelCopy
1820
import kotlin.test.Test
21+
import kotlin.test.assertFalse
1922
import kotlin.test.assertNotNull
23+
import kotlin.test.assertTrue
2024

2125
@Config(
2226
shadows = [ShadowPixelCopy::class],
@@ -32,7 +36,7 @@ class ScreenshotUtilTest {
3236
whenever(activity.isDestroyed).thenReturn(false)
3337

3438
val data =
35-
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
39+
ScreenshotUtils.captureScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
3640
assertNull(data)
3741
}
3842

@@ -44,7 +48,7 @@ class ScreenshotUtilTest {
4448
whenever(activity.window).thenReturn(mock<Window>())
4549

4650
val data =
47-
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
51+
ScreenshotUtils.captureScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
4852
assertNull(data)
4953
}
5054

@@ -60,7 +64,7 @@ class ScreenshotUtilTest {
6064
whenever(window.peekDecorView()).thenReturn(decorView)
6165

6266
val data =
63-
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
67+
ScreenshotUtils.captureScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
6468
assertNull(data)
6569
}
6670

@@ -81,7 +85,7 @@ class ScreenshotUtilTest {
8185
whenever(rootView.height).thenReturn(0)
8286

8387
val data =
84-
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
88+
ScreenshotUtils.captureScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
8589
assertNull(data)
8690
}
8791

@@ -94,7 +98,7 @@ class ScreenshotUtilTest {
9498
val buildInfoProvider = mock<BuildInfoProvider>()
9599
whenever(buildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.O)
96100

97-
val data = ScreenshotUtils.takeScreenshot(controller.get(), logger, buildInfoProvider)
101+
val data = ScreenshotUtils.captureScreenshot(controller.get(), logger, buildInfoProvider)
98102
assertNotNull(data)
99103
}
100104

@@ -107,9 +111,40 @@ class ScreenshotUtilTest {
107111
val buildInfoProvider = mock<BuildInfoProvider>()
108112
whenever(buildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.N)
109113

110-
val data = ScreenshotUtils.takeScreenshot(controller.get(), logger, buildInfoProvider)
114+
val data = ScreenshotUtils.captureScreenshot(controller.get(), logger, buildInfoProvider)
111115
assertNotNull(data)
112116
}
117+
118+
@Test
119+
fun `a null bitmap compresses into null`() {
120+
val bytes = ScreenshotUtils.compressBitmapToPng(null, NoOpLogger.getInstance())
121+
assertNull(bytes)
122+
}
123+
124+
@Test
125+
fun `a recycled bitmap compresses into null`() {
126+
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
127+
bitmap.recycle()
128+
129+
val bytes = ScreenshotUtils.compressBitmapToPng(bitmap, NoOpLogger.getInstance())
130+
assertNull(bytes)
131+
}
132+
133+
@Test
134+
fun `a valid bitmap compresses into a valid bytearray`() {
135+
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
136+
val bytes = ScreenshotUtils.compressBitmapToPng(bitmap, NoOpLogger.getInstance())
137+
assertNotNull(bytes)
138+
assertTrue(bytes.isNotEmpty())
139+
}
140+
141+
@Test
142+
fun `compressBitmapToPng recycles the supplied bitmap`() {
143+
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
144+
assertFalse(bitmap.isRecycled)
145+
ScreenshotUtils.compressBitmapToPng(bitmap, NoOpLogger.getInstance())
146+
assertTrue(bitmap.isRecycled)
147+
}
113148
}
114149

115150
class ExampleActivity : Activity() {

sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import io.sentry.protocol.SentryTransaction
2222
import org.junit.Assume
2323
import org.junit.Assume.assumeNotNull
2424
import org.junit.runner.RunWith
25+
import java.io.File
2526
import java.util.concurrent.TimeUnit
2627
import kotlin.test.Test
2728
import kotlin.test.assertEquals
@@ -229,6 +230,36 @@ class EnvelopeTests : BaseUiTest() {
229230
Thread.sleep(5000)
230231
}
231232

233+
@Test
234+
fun sendsNativeTransaction() {
235+
var optionsRef: SentryAndroidOptions? = null
236+
initSentry(true) { options ->
237+
options.tracesSampleRate = 1.0
238+
optionsRef = options
239+
}
240+
241+
// based on https://github.com/getsentry/sentry-native/blob/20d5d5f75f1f48228f2f47e2bb99b17f9996ebbf/ndk/lib/src/androidTest/java/io/sentry/ndk/SentryNdkTest.java#L131
242+
File(optionsRef!!.outboxPath, "14779dbf-b2f0-4c00-f4e5-4a287abc4267")
243+
.writeText(
244+
"""
245+
{"dsn":"https://key@sentry.io/proj","event_id":"729ff878-5539-458d-f657-a1acf423a127","sent_at":"2025-04-02T10:02:04.732577Z"}
246+
{"type":"transaction","length":1335}
247+
{"event_id":"729ff878-5539-458d-f657-a1acf423a127","platform":"native","transaction":"little.teapot","start_timestamp":"2025-04-02T10:02:04.731697Z","spans":[{"op":"littlest.teapot","span_id":"00028ba394454124","status":"ok","trace_id":"7160e289fe4c4496f02c72bbc7edb392","parent_span_id":"b0dc1649a8ec4101","description":null,"start_timestamp":"2025-04-02T10:02:04.732127Z","timestamp":"2025-04-02T10:02:04.732133Z"},{"op":"littler.teapot","span_id":"b0dc1649a8ec4101","status":"ok","trace_id":"7160e289fe4c4496f02c72bbc7edb392","parent_span_id":"7ad2e40529af4650","description":null,"start_timestamp":"2025-04-02T10:02:04.732118Z","data":{"span_data_says":"hi!"},"timestamp":"2025-04-02T10:02:04.732137Z"}],"type":"transaction","timestamp":"2025-04-02T10:02:04.732142Z","level":"info","contexts":{"trace":{"trace_id":"7160e289fe4c4496f02c72bbc7edb392","span_id":"7ad2e40529af4650","op":"Short and stout here is my handle and here is my spout","status":"ok","data":{"url":"https://example.com"}},"os":{"build":"android14-4-00257-g7e35917775b8-ab9964412","name":"Linux","version":"6.1.23"}},"release":"1.0.0","dist":"dist","environment":"production","sdk":{"name":"io.sentry.ndk","version":"0.8.3","packages":[{"name":"github:getsentry/sentry-native","version":"0.8.3"}],"integrations":["inproc"]},"tags":{},"extra":{},"breadcrumbs":[]}
248+
""".trimIndent()
249+
)
250+
251+
relayIdlingResource.increment()
252+
253+
relay.assert {
254+
assertFirstEnvelope {
255+
val event: SentryEvent = it.assertItem()
256+
it.assertNoOtherItems()
257+
assertEquals("little.teapot", event.transaction)
258+
}
259+
assertNoOtherEnvelopes()
260+
}
261+
}
262+
232263
private fun swipeList(times: Int) {
233264
repeat(times) {
234265
Thread.sleep(100)

sentry/api/sentry.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ public final class io/sentry/Attachment {
1111
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
1212
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
1313
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)V
14+
public fun <init> (Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
1415
public fun <init> ([BLjava/lang/String;)V
1516
public fun <init> ([BLjava/lang/String;Ljava/lang/String;)V
1617
public fun <init> ([BLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
1718
public fun <init> ([BLjava/lang/String;Ljava/lang/String;Z)V
19+
public static fun fromByteProvider (Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Z)Lio/sentry/Attachment;
1820
public static fun fromScreenshot ([B)Lio/sentry/Attachment;
1921
public static fun fromThreadDump ([B)Lio/sentry/Attachment;
2022
public static fun fromViewHierarchy (Lio/sentry/protocol/ViewHierarchy;)Lio/sentry/Attachment;
2123
public fun getAttachmentType ()Ljava/lang/String;
24+
public fun getByteProvider ()Ljava/util/concurrent/Callable;
2225
public fun getBytes ()[B
2326
public fun getContentType ()Ljava/lang/String;
2427
public fun getFilename ()Ljava/lang/String;

0 commit comments

Comments
 (0)