Skip to content

Commit 108d028

Browse files
authored
Merge branch 'main' into rz/fix/system-events-receiver-faster
2 parents 91fcdce + 9201bd3 commit 108d028

File tree

19 files changed

+312
-44
lines changed

19 files changed

+312
-44
lines changed

CHANGELOG.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@
22

33
## Unreleased
44

5-
### Improvements
5+
### Features
66

7-
- Make `SystemEventsBreadcrumbsIntegration` faster ([#4330](https://github.com/getsentry/sentry-java/pull/4330))
7+
- Add `CoroutineExceptionHandler` for reporting uncaught exceptions in coroutines to Sentry ([#4259](https://github.com/getsentry/sentry-java/pull/4259))
8+
- This is now part of `sentry-kotlin-extensions` and can be used together with `SentryContext` when launching a coroutine
9+
- Any exceptions thrown in a coroutine when using the handler will be captured (not rethrown!) and reported to Sentry
10+
- It's also possible to extend `CoroutineExceptionHandler` to implement custom behavior in addition to the one we provide by default
811

912
### Fixes
1013

1114
- Use thread context classloader when available ([#4320](https://github.com/getsentry/sentry-java/pull/4320))
1215
- This ensures correct resource loading in environments like Spring Boot where the thread context classloader is used for resource loading.
16+
- Improve low memory breadcrumb capturing ([#4325](https://github.com/getsentry/sentry-java/pull/4325))
17+
- Fix do not initialize SDK for Jetpack Compose Preview builds ([#4324](https://github.com/getsentry/sentry-java/pull/4324))
18+
19+
### Improvements
20+
21+
- Make `SystemEventsBreadcrumbsIntegration` faster ([#4330](https://github.com/getsentry/sentry-java/pull/4330))
1322

1423
## 8.7.0
1524

buildSrc/src/main/java/Config.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ object Config {
121121

122122
val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
123123

124+
val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
125+
124126
val fragment = "androidx.fragment:fragment-ktx:1.3.5"
125127

126128
val reactorCore = "io.projectreactor:reactor-core:3.5.3"
@@ -214,6 +216,7 @@ object Config {
214216
val leakCanaryInstrumentation = "com.squareup.leakcanary:leakcanary-android-instrumentation:2.14"
215217
val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.6.8"
216218
val okio = "com.squareup.okio:okio:1.13.0"
219+
val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1"
217220
}
218221

219222
object QualityPlugins {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ public final class io/sentry/android/core/BuildInfoProvider {
185185
}
186186

187187
public final class io/sentry/android/core/ContextUtils {
188+
public static fun appIsLibraryForComposePreview (Landroid/content/Context;)Z
188189
public static fun getApplicationContext (Landroid/content/Context;)Landroid/content/Context;
189190
public static fun isForegroundImportance ()Z
190191
}

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

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import io.sentry.Integration;
1313
import io.sentry.SentryLevel;
1414
import io.sentry.SentryOptions;
15+
import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
16+
import io.sentry.android.core.internal.util.Debouncer;
1517
import io.sentry.android.core.internal.util.DeviceOrientations;
1618
import io.sentry.protocol.Device;
1719
import io.sentry.util.Objects;
@@ -24,10 +26,17 @@
2426
public final class AppComponentsBreadcrumbsIntegration
2527
implements Integration, Closeable, ComponentCallbacks2 {
2628

29+
private static final long DEBOUNCE_WAIT_TIME_MS = 60 * 1000;
30+
// pre-allocate hint to avoid creating it every time for the low memory case
31+
private static final @NotNull Hint EMPTY_HINT = new Hint();
32+
2733
private final @NotNull Context context;
2834
private @Nullable IScopes scopes;
2935
private @Nullable SentryAndroidOptions options;
3036

37+
private final @NotNull Debouncer trimMemoryDebouncer =
38+
new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS, 0);
39+
3140
public AppComponentsBreadcrumbsIntegration(final @NotNull Context context) {
3241
this.context =
3342
Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required");
@@ -91,42 +100,43 @@ public void onConfigurationChanged(@NotNull Configuration newConfig) {
91100

92101
@Override
93102
public void onLowMemory() {
94-
final long now = System.currentTimeMillis();
95-
executeInBackground(() -> captureLowMemoryBreadcrumb(now, null));
103+
// we do this in onTrimMemory below already, this is legacy API (14 or below)
96104
}
97105

98106
@Override
99107
public void onTrimMemory(final int level) {
108+
if (level < TRIM_MEMORY_BACKGROUND) {
109+
// only add breadcrumb if TRIM_MEMORY_BACKGROUND, TRIM_MEMORY_MODERATE or
110+
// TRIM_MEMORY_COMPLETE.
111+
// Release as much memory as the process can.
112+
113+
// TRIM_MEMORY_UI_HIDDEN, TRIM_MEMORY_RUNNING_MODERATE, TRIM_MEMORY_RUNNING_LOW and
114+
// TRIM_MEMORY_RUNNING_CRITICAL.
115+
// Release any memory that your app doesn't need to run.
116+
// So they are still not so critical at the point of killing the process.
117+
// https://developer.android.com/topic/performance/memory
118+
return;
119+
}
120+
121+
if (trimMemoryDebouncer.checkForDebounce()) {
122+
// if we received trim_memory within 1 minute time, ignore this call
123+
return;
124+
}
125+
100126
final long now = System.currentTimeMillis();
101127
executeInBackground(() -> captureLowMemoryBreadcrumb(now, level));
102128
}
103129

104-
private void captureLowMemoryBreadcrumb(final long timeMs, final @Nullable Integer level) {
130+
private void captureLowMemoryBreadcrumb(final long timeMs, final int level) {
105131
if (scopes != null) {
106132
final Breadcrumb breadcrumb = new Breadcrumb(timeMs);
107-
if (level != null) {
108-
// only add breadcrumb if TRIM_MEMORY_BACKGROUND, TRIM_MEMORY_MODERATE or
109-
// TRIM_MEMORY_COMPLETE.
110-
// Release as much memory as the process can.
111-
112-
// TRIM_MEMORY_UI_HIDDEN, TRIM_MEMORY_RUNNING_MODERATE, TRIM_MEMORY_RUNNING_LOW and
113-
// TRIM_MEMORY_RUNNING_CRITICAL.
114-
// Release any memory that your app doesn't need to run.
115-
// So they are still not so critical at the point of killing the process.
116-
// https://developer.android.com/topic/performance/memory
117-
118-
if (level < TRIM_MEMORY_BACKGROUND) {
119-
return;
120-
}
121-
breadcrumb.setData("level", level);
122-
}
123-
124133
breadcrumb.setType("system");
125134
breadcrumb.setCategory("device.event");
126135
breadcrumb.setMessage("Low memory");
127136
breadcrumb.setData("action", "LOW_MEMORY");
137+
breadcrumb.setData("level", level);
128138
breadcrumb.setLevel(SentryLevel.WARNING);
129-
scopes.addBreadcrumb(breadcrumb);
139+
scopes.addBreadcrumb(breadcrumb, EMPTY_HINT);
130140
}
131141
}
132142

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import android.annotation.SuppressLint;
88
import android.app.ActivityManager;
99
import android.content.BroadcastReceiver;
10+
import android.content.ComponentName;
1011
import android.content.Context;
1112
import android.content.Intent;
1213
import android.content.IntentFilter;
@@ -27,6 +28,7 @@
2728
import java.io.IOException;
2829
import java.util.Arrays;
2930
import java.util.HashMap;
31+
import java.util.List;
3032
import java.util.Map;
3133
import org.jetbrains.annotations.ApiStatus;
3234
import org.jetbrains.annotations.NotNull;
@@ -285,6 +287,36 @@ public static boolean isForegroundImportance() {
285287
return isForegroundImportance.getValue();
286288
}
287289

290+
/**
291+
* Determines if the app is a packaged android library for running Compose Preview Mode
292+
*
293+
* @param context the context
294+
* @return true, if the app is actually a library running as an app for Compose Preview Mode
295+
*/
296+
@ApiStatus.Internal
297+
public static boolean appIsLibraryForComposePreview(final @NotNull Context context) {
298+
// Jetpack Compose Preview (aka "Run Preview on Device")
299+
// uses the androidTest flavor for android library modules,
300+
// so let's fail-fast by checking this first
301+
if (context.getPackageName().endsWith(".test")) {
302+
try {
303+
final @NotNull ActivityManager activityManager =
304+
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
305+
final @NotNull List<ActivityManager.AppTask> appTasks = activityManager.getAppTasks();
306+
for (final ActivityManager.AppTask task : appTasks) {
307+
final @Nullable ComponentName component = task.getTaskInfo().baseIntent.getComponent();
308+
if (component != null
309+
&& component.getClassName().equals("androidx.compose.ui.tooling.PreviewActivity")) {
310+
return true;
311+
}
312+
}
313+
} catch (Throwable t) {
314+
// ignored
315+
}
316+
}
317+
return false;
318+
}
319+
288320
/**
289321
* Get the device's current kernel version, as a string. Attempts to read /proc/version, and falls
290322
* back to the 'os.version' System Property.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public boolean onCreate() {
2121
logger.log(SentryLevel.FATAL, "App. Context from ContentProvider is null");
2222
return false;
2323
}
24-
if (ManifestMetadataReader.isAutoInit(context, logger)) {
24+
25+
if (ManifestMetadataReader.isAutoInit(context, logger)
26+
&& !ContextUtils.appIsLibraryForComposePreview(context)) {
2527
SentryAndroid.init(context, logger);
2628
SentryIntegrationPackageStorage.getInstance().addIntegration("AutoInit");
2729
}

sentry-android-core/src/test/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegrationTest.kt

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import org.mockito.kotlin.check
1515
import org.mockito.kotlin.mock
1616
import org.mockito.kotlin.never
1717
import org.mockito.kotlin.verify
18+
import org.mockito.kotlin.verifyNoMoreInteractions
1819
import org.mockito.kotlin.whenever
1920
import java.lang.NullPointerException
2021
import kotlin.test.Test
@@ -95,24 +96,6 @@ class AppComponentsBreadcrumbsIntegrationTest {
9596
sut.close()
9697
}
9798

98-
@Test
99-
fun `When low memory event, a breadcrumb with type, category and level should be set`() {
100-
val sut = fixture.getSut()
101-
val options = SentryAndroidOptions().apply {
102-
executorService = ImmediateExecutorService()
103-
}
104-
val scopes = mock<IScopes>()
105-
sut.register(scopes, options)
106-
sut.onLowMemory()
107-
verify(scopes).addBreadcrumb(
108-
check<Breadcrumb> {
109-
assertEquals("device.event", it.category)
110-
assertEquals("system", it.type)
111-
assertEquals(SentryLevel.WARNING, it.level)
112-
}
113-
)
114-
}
115-
11699
@Test
117100
fun `When trim memory event with level, a breadcrumb with type, category and level should be set`() {
118101
val sut = fixture.getSut()
@@ -127,7 +110,8 @@ class AppComponentsBreadcrumbsIntegrationTest {
127110
assertEquals("device.event", it.category)
128111
assertEquals("system", it.type)
129112
assertEquals(SentryLevel.WARNING, it.level)
130-
}
113+
},
114+
anyOrNull()
131115
)
132116
}
133117

@@ -162,4 +146,26 @@ class AppComponentsBreadcrumbsIntegrationTest {
162146
anyOrNull()
163147
)
164148
}
149+
150+
@Test
151+
fun `low memory changes are debounced`() {
152+
val sut = fixture.getSut()
153+
154+
val scopes = mock<IScopes>()
155+
val options = SentryAndroidOptions().apply {
156+
executorService = ImmediateExecutorService()
157+
}
158+
sut.register(scopes, options)
159+
sut.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
160+
sut.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL)
161+
162+
// should only add the first crumb
163+
verify(scopes).addBreadcrumb(
164+
check<Breadcrumb> {
165+
assertEquals(it.data["level"], 40)
166+
},
167+
anyOrNull()
168+
)
169+
verifyNoMoreInteractions(scopes)
170+
}
165171
}

sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import android.app.ActivityManager
55
import android.app.ActivityManager.MemoryInfo
66
import android.app.ActivityManager.RunningAppProcessInfo
77
import android.content.BroadcastReceiver
8+
import android.content.ComponentName
89
import android.content.Context
10+
import android.content.Intent
911
import android.content.IntentFilter
1012
import android.content.pm.ApplicationInfo
1113
import android.content.pm.PackageInfo
@@ -270,4 +272,35 @@ class ContextUtilsTest {
270272
val appContext = ContextUtils.getApplicationContext(contextMock)
271273
assertSame(appContextMock, appContext)
272274
}
275+
276+
@Test
277+
fun `appIsLibraryForComposePreview is correctly determined`() {
278+
fun getMockContext(
279+
packageName: String,
280+
activityClassName: String
281+
): Context {
282+
val context = mock<Context>()
283+
val activityManager = mock<ActivityManager>()
284+
whenever(context.packageName).thenReturn(packageName)
285+
whenever(context.getSystemService(eq(Context.ACTIVITY_SERVICE))).thenReturn(
286+
activityManager
287+
)
288+
val taskInfo = ActivityManager.RecentTaskInfo()
289+
taskInfo.baseIntent = Intent().setComponent(
290+
ComponentName(
291+
"com.example.library",
292+
activityClassName
293+
)
294+
)
295+
val appTask = mock<ActivityManager.AppTask>()
296+
whenever(appTask.taskInfo).thenReturn(taskInfo)
297+
whenever(activityManager.appTasks).thenReturn(listOf(appTask))
298+
299+
return context
300+
}
301+
302+
assertTrue(ContextUtils.appIsLibraryForComposePreview(getMockContext("com.example.library.test", "androidx.compose.ui.tooling.PreviewActivity")))
303+
assertFalse(ContextUtils.appIsLibraryForComposePreview(getMockContext("com.example.library.test", "com.example.HomeActivity")))
304+
assertFalse(ContextUtils.appIsLibraryForComposePreview(getMockContext("com.example.library", "androidx.compose.ui.tooling.PreviewActivity")))
305+
}
273306
}

sentry-android-core/src/test/java/io/sentry/android/core/SentryInitProviderTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
77
import io.sentry.Sentry
88
import io.sentry.test.callMethod
99
import org.junit.runner.RunWith
10+
import org.mockito.Mockito
11+
import org.mockito.kotlin.any
1012
import kotlin.test.BeforeTest
1113
import kotlin.test.Test
1214
import kotlin.test.assertFailsWith
1315
import kotlin.test.assertFalse
1416
import kotlin.test.assertTrue
17+
import kotlin.use
1518

1619
@RunWith(AndroidJUnit4::class)
1720
class SentryInitProviderTest {
@@ -153,6 +156,24 @@ class SentryInitProviderTest {
153156
assertFalse(sentryOptions.isEnableNdk)
154157
}
155158

159+
@Test
160+
fun `skips init in compose preview mode`() {
161+
val providerInfo = ProviderInfo()
162+
163+
assertFalse(Sentry.isEnabled())
164+
providerInfo.authority = AUTHORITY
165+
166+
val metaData = Bundle()
167+
metaData.putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123")
168+
val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metaData)
169+
170+
Mockito.mockStatic(ContextUtils::class.java).use { contextUtils ->
171+
contextUtils.`when`<Boolean> { ContextUtils.appIsLibraryForComposePreview(any()) }.thenReturn(true)
172+
sentryInitProvider.attachInfo(mockContext, providerInfo)
173+
}
174+
assertFalse(Sentry.isEnabled())
175+
}
176+
156177
companion object {
157178
private const val AUTHORITY = "io.sentry.sample.SentryInitProvider"
158179
}

sentry-apollo-4/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dependencies {
4141
testImplementation(Config.TestLibs.mockitoInline)
4242
testImplementation(Config.TestLibs.mockWebserver)
4343
testImplementation(Config.Libs.apolloKotlin4)
44-
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
44+
testImplementation(Config.TestLibs.coroutinesTest)
4545
testImplementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0")
4646
}
4747

0 commit comments

Comments
 (0)