Skip to content

Commit 623643d

Browse files
authored
Merge branch 'main' into rz/fix/replay-network-body-size
2 parents 8631ef2 + 8183500 commit 623643d

29 files changed

Lines changed: 146 additions & 65 deletions

File tree

CHANGELOG.md

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

77
- Session Replay: Fix network detail response body size being unknown for gzip-compressed responses ([#5592](https://github.com/getsentry/sentry-java/pull/5592))
88
- Session Replay: Release `MediaMuxer` when a replay segment has no encodable frames to avoid a resource leak ([#5583](https://github.com/getsentry/sentry-java/pull/5583))
9+
- Fix crash when `getHistoricalProcessStartReasons` is called from an isolated or wrong-userId process ([#5597](https://github.com/getsentry/sentry-java/pull/5597))
910

1011
## 8.44.1
1112

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import io.sentry.gradle.SystemTestExtension
2+
import org.gradle.api.tasks.ClasspathNormalizer
3+
4+
val systemTest = extensions.create<SystemTestExtension>("sentrySystemTest")
5+
6+
// The sample system tests launch the packaged app (war/shadowJar/bootJar) from build/libs as a
7+
// separate process, so the archive is a real input even though it is not on the test classpath.
8+
// Agent-based samples are additionally launched with -javaagent:<otel agent>, another runtime
9+
// input not on the classpath. See test/system-test-runner.py.
10+
tasks.matching { it.name == "systemTest" }.configureEach {
11+
val archiveTask =
12+
listOf("war", "shadowJar", "bootJar").firstOrNull { it in tasks.names }
13+
?: throw GradleException(
14+
"io.sentry.systemtest is applied to $path but none of war/shadowJar/bootJar " +
15+
"exist to provide the launched app archive for the systemTest task"
16+
)
17+
// Declaring the archive as an input also wires the dependency on its producing task.
18+
inputs
19+
.files(tasks.named(archiveTask))
20+
.withPropertyName("appArchive")
21+
.withNormalizer(ClasspathNormalizer::class.java)
22+
23+
if (systemTest.usesOpenTelemetryAgent.get()) {
24+
// The runner builds the agent and launches the app with -javaagent before invoking this task,
25+
// so the agent jar is tracked for content only (by path, no cross-project task dependency): a
26+
// change to it makes systemTest out of date even though it runs outside the test JVM.
27+
val version = providers.gradleProperty("versionName").get()
28+
inputs
29+
.files(
30+
rootProject.layout.projectDirectory.file(
31+
"sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/" +
32+
"sentry-opentelemetry-agent-$version.jar"
33+
)
34+
)
35+
.withPropertyName("openTelemetryAgent")
36+
.withNormalizer(ClasspathNormalizer::class.java)
37+
}
38+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.sentry.gradle
2+
3+
import org.gradle.api.provider.Property
4+
5+
/** Configuration for the `io.sentry.systemtest` convention plugin. */
6+
abstract class SystemTestExtension {
7+
/**
8+
* Set to `true` for samples that the system-test runner launches with the Sentry OpenTelemetry
9+
* Java agent (`-javaagent`). The agent jar is then tracked as a `systemTest` input so the task
10+
* re-runs when the agent changes, even though it is started outside the test JVM.
11+
*/
12+
abstract val usesOpenTelemetryAgent: Property<Boolean>
13+
14+
init {
15+
usesOpenTelemetryAgent.convention(false)
16+
}
17+
}

sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.os.Handler;
1212
import android.os.Looper;
1313
import android.os.SystemClock;
14+
import android.util.Log;
1415
import androidx.annotation.NonNull;
1516
import androidx.annotation.Nullable;
1617
import androidx.annotation.VisibleForTesting;
@@ -467,18 +468,28 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
467468
final @Nullable ActivityManager activityManager =
468469
(ActivityManager) application.getSystemService(Context.ACTIVITY_SERVICE);
469470
if (activityManager != null) {
470-
final List<ApplicationStartInfo> historicalProcessStartReasons =
471-
activityManager.getHistoricalProcessStartReasons(1);
472-
if (!historicalProcessStartReasons.isEmpty()) {
473-
final @NotNull ApplicationStartInfo info = historicalProcessStartReasons.get(0);
474-
cachedStartInfo = info;
475-
if (info.getStartupState() == ApplicationStartInfo.STARTUP_STATE_STARTED) {
476-
if (info.getStartType() == ApplicationStartInfo.START_TYPE_COLD) {
477-
appStartType = AppStartType.COLD;
478-
} else {
479-
appStartType = AppStartType.WARM;
471+
try {
472+
final List<ApplicationStartInfo> historicalProcessStartReasons =
473+
activityManager.getHistoricalProcessStartReasons(1);
474+
if (!historicalProcessStartReasons.isEmpty()) {
475+
final @NotNull ApplicationStartInfo info = historicalProcessStartReasons.get(0);
476+
cachedStartInfo = info;
477+
if (info.getStartupState() == ApplicationStartInfo.STARTUP_STATE_STARTED) {
478+
if (info.getStartType() == ApplicationStartInfo.START_TYPE_COLD) {
479+
appStartType = AppStartType.COLD;
480+
} else {
481+
appStartType = AppStartType.WARM;
482+
}
480483
}
481484
}
485+
} catch (RuntimeException ignored) {
486+
// getHistoricalProcessStartReasons may throw different kinds of exceptions, namely:
487+
// - SecurityException when called from an isolated process
488+
// - IllegalArgumentException when called with a wrong userId
489+
// - others
490+
// See impl:
491+
// https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=10866-10893
492+
Log.w("AppStartMetrics", ignored); // no logger instance here, so we just Log
482493
}
483494
}
484495
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,24 @@ class SentryShadowActivityManager {
1212
companion object {
1313
private var historicalProcessStartReasons: List<ApplicationStartInfo> = emptyList()
1414
private var importance: Int = RunningAppProcessInfo.IMPORTANCE_FOREGROUND
15+
private var historicalProcessStartReasonsException: RuntimeException? = null
1516

1617
fun setHistoricalProcessStartReasons(startReasons: List<ApplicationStartInfo>) {
1718
historicalProcessStartReasons = startReasons
1819
}
1920

21+
fun setHistoricalProcessStartReasonsException(exception: RuntimeException) {
22+
historicalProcessStartReasonsException = exception
23+
}
24+
2025
fun setImportance(importance: Int) {
2126
this.importance = importance
2227
}
2328

2429
fun reset() {
2530
historicalProcessStartReasons = emptyList()
2631
importance = RunningAppProcessInfo.IMPORTANCE_FOREGROUND
32+
historicalProcessStartReasonsException = null
2733
}
2834

2935
@Implementation
@@ -35,6 +41,7 @@ class SentryShadowActivityManager {
3541

3642
@Implementation
3743
fun getHistoricalProcessStartReasons(maxNum: Int): List<ApplicationStartInfo> {
44+
historicalProcessStartReasonsException?.let { throw it }
3845
return historicalProcessStartReasons.take(maxNum)
3946
}
4047
}

sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTestApi35.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,20 @@ class AppStartMetricsTestApi35 {
249249
assertNull(metrics.appStartReason)
250250
}
251251

252+
@Test
253+
fun `does not crash when getHistoricalProcessStartReasons throws RuntimeException`() {
254+
SentryShadowActivityManager.setHistoricalProcessStartReasonsException(
255+
RuntimeException("isolated process")
256+
)
257+
val metrics = AppStartMetrics.getInstance()
258+
259+
val app = ApplicationProvider.getApplicationContext<Application>()
260+
metrics.registerLifecycleCallbacks(app)
261+
262+
assertEquals(AppStartMetrics.AppStartType.UNKNOWN, metrics.appStartType)
263+
assertNull(metrics.appStartReason)
264+
}
265+
252266
private fun waitForMainLooperIdle() {
253267
Handler(Looper.getMainLooper()).post {}
254268
Shadows.shadowOf(Looper.getMainLooper()).idle()

sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -228,18 +228,24 @@ class ComposeMaskingOptionsTest {
228228

229229
val textNodes = activity.get().collectNodesOfType<TextViewHierarchyNode>(options)
230230
assertEquals(4, textNodes.size) // [TextField, Text, Button, Activity Title]
231-
textNodes.forEach {
232-
if ((it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text == "Make Request") {
233-
assertFalse(
234-
it.shouldMask,
235-
"Node with text ${(it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text} should not be masked",
236-
)
237-
} else {
238-
assertTrue(
239-
it.shouldMask,
240-
"Node with text ${(it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text} should be masked",
241-
)
231+
232+
val unmaskNode =
233+
textNodes.first {
234+
(it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text == "Make Request"
242235
}
236+
assertTrue(unmaskNode.isVisible, "The unmasked node must be visible for the test to be valid")
237+
assertFalse(unmaskNode.shouldMask, "Node with sentryReplayUnmask() should not be masked")
238+
239+
// Robolectric may intermittently report zero bounds for some nodes when running
240+
// the full test class, making them invisible (shouldMask = isVisible && ...).
241+
// Assert that all other visible nodes remain masked.
242+
val otherVisibleNodes = textNodes.filter { it !== unmaskNode && it.isVisible }
243+
assertTrue(otherVisibleNodes.isNotEmpty(), "Expected at least one other visible text node")
244+
otherVisibleNodes.forEach {
245+
assertTrue(
246+
it.shouldMask,
247+
"Node with text ${(it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text} should be masked",
248+
)
243249
}
244250
}
245251

sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
alias(libs.plugins.kotlin.jvm)
77
alias(libs.plugins.gradle.versions)
88
alias(libs.plugins.shadow)
9+
id("io.sentry.systemtest")
910
}
1011

1112
application { mainClass.set("io.sentry.samples.console.Main") }
@@ -71,8 +72,6 @@ tasks.register<Test>("systemTest").configure {
7172
testClassesDirs = test.output.classesDirs
7273
classpath = test.runtimeClasspath
7374

74-
outputs.upToDateWhen { false }
75-
7675
maxParallelForks = 1
7776

7877
// Cap JVM args per test

sentry-samples/sentry-samples-console-otlp/build.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
alias(libs.plugins.kotlin.jvm)
77
alias(libs.plugins.gradle.versions)
88
alias(libs.plugins.shadow)
9+
id("io.sentry.systemtest")
910
}
1011

1112
application { mainClass.set("io.sentry.samples.console.Main") }
@@ -74,8 +75,6 @@ tasks.register<Test>("systemTest").configure {
7475
testClassesDirs = test.output.classesDirs
7576
classpath = test.runtimeClasspath
7677

77-
outputs.upToDateWhen { false }
78-
7978
maxParallelForks = 1
8079

8180
// Cap JVM args per test

sentry-samples/sentry-samples-console/build.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
alias(libs.plugins.kotlin.jvm)
77
alias(libs.plugins.gradle.versions)
88
alias(libs.plugins.shadow)
9+
id("io.sentry.systemtest")
910
}
1011

1112
application { mainClass.set("io.sentry.samples.console.Main") }
@@ -75,8 +76,6 @@ tasks.register<Test>("systemTest").configure {
7576
testClassesDirs = test.output.classesDirs
7677
classpath = test.runtimeClasspath
7778

78-
outputs.upToDateWhen { false }
79-
8079
maxParallelForks = 1
8180

8281
// Cap JVM args per test

0 commit comments

Comments
 (0)