From e504c197b5a4e55e229dc8ee4e9e12fc2b5cf4ae Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 27 Oct 2026 13:40:40 +0100 Subject: [PATCH] test: Fix follow-ups from the sample tests stack review Fix issues the review of the sample tests stack found in the new in-module end-to-end tests: - MockSentryServer keeps the error when it can't read a body and rethrows it while waiting, so a serialization bug no longer looks like a missing envelope. - ChildJvmApp only takes a port that two reads agree on. A partly written port such as "54" of "54321" parsed and pointed the test at the wrong port. - The agentless test starts a span through the OpenTelemetry API and checks that it reaches Sentry. Before, it passed with the OpenTelemetry wiring broken. - The Person tests use id 11 and match their transaction by URL, so envelopes from the tracing tests' /person/1 calls can't satisfy them. - The Boot 2 Kafka test shares the suite's context and broker, as on Boot 3 and 4. Its second context re-ran Sentry.init and made later tests depend on the run order. Boot 2.4 to 2.6 were the reason for the split and are gone. - Tests fail clearly when a system property the Gradle task sets is missing, rather than running with -javaagent:null. - Remove the Boot 2 version gating from the samples, the runner's files from .gitignore, and the unused jackson-kotlin entry. Co-Authored-By: Nelson Osacky --- .claude/skills/test/SKILL.md | 1 + .gitignore | 3 -- gradle/libs.versions.toml | 1 - .../opentelemetry/agent/AgentSmokeTest.kt | 3 +- .../agentless/AgentlessEndToEndTest.kt | 11 +++++- .../build.gradle.kts | 39 ++----------------- .../build.gradle.kts | 39 ++----------------- .../build.gradle.kts | 27 +------------ .../build.gradle.kts | 39 ++----------------- .../integrationtest/PersonIntegrationTest.kt | 16 ++++++-- .../otelagentintegrationtest/AgentApp.kt | 7 ++-- .../PersonIntegrationTest.kt | 12 ++++-- .../PersonIntegrationTest.kt | 16 ++++++-- .../PersonIntegrationTest.kt | 16 ++++++-- .../integrationtest/PersonIntegrationTest.kt | 16 ++++++-- .../otelagentintegrationtest/AgentApp.kt | 7 ++-- .../PersonIntegrationTest.kt | 12 ++++-- .../PersonIntegrationTest.kt | 16 ++++++-- .../boot/integrationtest/IntegrationTest.kt | 16 +++++--- .../boot/integrationtest/KafkaController.kt | 5 --- .../integrationtest/KafkaIntegrationTest.kt | 16 +------- .../integrationtest/PersonIntegrationTest.kt | 16 ++++++-- .../boot/otelagentintegrationtest/AgentApp.kt | 7 ++-- .../PersonIntegrationTest.kt | 12 ++++-- .../PersonIntegrationTest.kt | 16 ++++++-- .../main/kotlin/io/sentry/test/ChildJvmApp.kt | 14 ++++++- .../kotlin/io/sentry/test/MockSentryServer.kt | 18 ++++++++- .../io/sentry/test/MockSentryServerTest.kt | 15 +++++++ 28 files changed, 202 insertions(+), 214 deletions(-) diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md index 0a066c76de0..5e19619b1b2 100644 --- a/.claude/skills/test/SKILL.md +++ b/.claude/skills/test/SKILL.md @@ -32,6 +32,7 @@ Extract the module name and optional test class filter from the argument. | Signal | Test Type | |--------|-----------| | Path contains `/src/integrationTest*/` or `/src/agentFixture/` | Integration test suite | +| Module name only, and the user asks for integration or end-to-end tests | Integration test suite: each `src/integrationTest*/` directory of the module | | Path contains `sentry-samples/`, or module name starts with `sentry-samples-` | None: samples have no tests | | Everything else | Unit test | diff --git a/.gitignore b/.gitignore index f252087a5ab..b6301586e81 100644 --- a/.gitignore +++ b/.gitignore @@ -22,9 +22,6 @@ distributions/ *.vscode/ sentry-spring-boot-starter-jakarta/src/main/resources/META-INF/spring.factories sentry-samples/sentry-samples-spring-boot-jakarta/spy.log -sentry-mock-server.txt -tomcat-server.txt -spring-server.txt *.pid spy.log .kotlin diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09ac7a2113e..0d978c34f4a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -143,7 +143,6 @@ graphql-java22 = { module = "com.graphql-java:graphql-java", version = "22.1" } graphql-java24 = { module = "com.graphql-java:graphql-java", version = "24.0" } jackson-bom = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "jackson" } jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } -jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } jetbrains-annotations = { module = "org.jetbrains:annotations", version = "23.0.0" } kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } diff --git a/sentry-opentelemetry/sentry-opentelemetry-agent/src/test/kotlin/io/sentry/opentelemetry/agent/AgentSmokeTest.kt b/sentry-opentelemetry/sentry-opentelemetry-agent/src/test/kotlin/io/sentry/opentelemetry/agent/AgentSmokeTest.kt index 2764a7d183a..a401af0f32a 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agent/src/test/kotlin/io/sentry/opentelemetry/agent/AgentSmokeTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-agent/src/test/kotlin/io/sentry/opentelemetry/agent/AgentSmokeTest.kt @@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat import io.sentry.opentelemetry.agent.fixture.HelloServer import io.sentry.test.ChildJvmApp import io.sentry.test.MockSentryServer +import io.sentry.test.requiredTestProperty import io.sentry.test.transaction import java.io.File import java.net.URL @@ -32,7 +33,7 @@ class AgentSmokeTest { mainClass = HelloServer::class.java.name, classpath = File(HelloServer::class.java.protectionDomain.codeSource.location.toURI()).path, - jvmArgs = listOf("-javaagent:${System.getProperty("sentry.test.agentJar")}"), + jvmArgs = listOf("-javaagent:${requiredTestProperty("sentry.test.agentJar")}"), env = mapOf( "SENTRY_DSN" to server.dsn, diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentless/src/test/kotlin/io/sentry/opentelemetry/agentless/AgentlessEndToEndTest.kt b/sentry-opentelemetry/sentry-opentelemetry-agentless/src/test/kotlin/io/sentry/opentelemetry/agentless/AgentlessEndToEndTest.kt index 8f9d5d0fb8a..6d519ed4174 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentless/src/test/kotlin/io/sentry/opentelemetry/agentless/AgentlessEndToEndTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-agentless/src/test/kotlin/io/sentry/opentelemetry/agentless/AgentlessEndToEndTest.kt @@ -41,7 +41,7 @@ class AgentlessEndToEndTest { } @Test - fun `Sentry set up by OpenTelemetry reports events, a profiled transaction and metrics`() { + fun `Sentry set up by OpenTelemetry reports events, a profiled transaction, OpenTelemetry spans and metrics`() { // The customizer's Sentry.init reads these as external configuration. System.setProperty("sentry.dsn", server.dsn) System.setProperty("sentry.traces-sample-rate", "1.0") @@ -107,6 +107,11 @@ class AgentlessEndToEndTest { Thread.sleep(100) span.finish() transaction.finish() + // A span started through the OpenTelemetry API only reaches Sentry if the customizer wired + // Sentry's span processor into the SDK. + val otelSpan = + GlobalOpenTelemetry.getTracer("agentless-test").spanBuilder("otel span").startSpan() + otelSpan.end() Sentry.flush(5000) // Sentry exports OpenTelemetry spans in batches every 5 seconds. openTelemetry.sdkTracerProvider.forceFlush().join(5, TimeUnit.SECONDS) @@ -138,6 +143,10 @@ class AgentlessEndToEndTest { val sent = server.awaitEnvelope { it.transaction()?.transaction == "transaction name" }.transaction()!! assertThat(sent.spans.map { it.op }).contains("child") + val otelTransaction = + server.awaitEnvelope { it.transaction()?.transaction == "otel span" }.transaction()!! + assertThat(otelTransaction.contexts.trace!!.traceId.toString()) + .isEqualTo(otelSpan.spanContext.traceId) val profilerId = sent.contexts.profile?.profilerId assertThat(profilerId).isNotNull() server.awaitEnvelope(timeoutMillis = 30_000) { it.profileChunk()?.profilerId == profilerId } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts index a5ccb8d3224..62c5bcbd3aa 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts @@ -25,18 +25,6 @@ group = "io.sentry.sample.spring-boot" version = "0.0.1-SNAPSHOT" -fun springBoot2SupportsOptionalIntegrations(): Boolean { - val version = libs.versions.springboot2.get().removeSuffix(".RELEASE") - val parts = version.split(".").map { it.toIntOrNull() ?: 0 } - val major = parts.getOrElse(0) { 0 } - val minor = parts.getOrElse(1) { 0 } - return major > 2 || (major == 2 && minor >= 7) -} - -val includeGraphql = - !project.hasProperty("excludeGraphql") && springBoot2SupportsOptionalIntegrations() -val includeKafka = !project.hasProperty("excludeKafka") && springBoot2SupportsOptionalIntegrations() - tasks.withType().configureEach { kotlin { compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") @@ -49,9 +37,7 @@ dependencies { implementation(libs.springboot.starter) implementation(libs.springboot.starter.actuator) implementation(libs.springboot.starter.aop) - if (includeGraphql) { - implementation(libs.springboot.starter.graphql) - } + implementation(libs.springboot.starter.graphql) implementation(libs.springboot.starter.jdbc) implementation(libs.springboot.starter.quartz) implementation(libs.springboot.starter.security) @@ -63,17 +49,13 @@ dependencies { implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) implementation(projects.sentrySpringBootStarter) implementation(projects.sentryLogback) - if (includeGraphql) { - implementation(projects.sentryGraphql) - } + implementation(projects.sentryGraphql) implementation(projects.sentryQuartz) implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentlessSpring) implementation(projects.sentryAsyncProfiler) - if (includeKafka) { - implementation(libs.spring.kafka2) - implementation(projects.sentryKafka) - } + implementation(libs.spring.kafka2) + implementation(projects.sentryKafka) // database query tracing implementation(projects.sentryJdbc) @@ -101,16 +83,3 @@ tasks.jar { } tasks.startScripts { dependsOn(tasks.shadowJar) } - -configure { - main { - if (!includeGraphql) { - java.exclude("**/graphql/**") - resources.exclude("graphql/**") - } - if (!includeKafka) { - java.exclude("**/queues/kafka/**") - resources.exclude("application-kafka.properties") - } - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts index dbb4d66dea3..652727f7468 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts @@ -25,18 +25,6 @@ group = "io.sentry.sample.spring-boot" version = "0.0.1-SNAPSHOT" -fun springBoot2SupportsOptionalIntegrations(): Boolean { - val version = libs.versions.springboot2.get().removeSuffix(".RELEASE") - val parts = version.split(".").map { it.toIntOrNull() ?: 0 } - val major = parts.getOrElse(0) { 0 } - val minor = parts.getOrElse(1) { 0 } - return major > 2 || (major == 2 && minor >= 7) -} - -val includeGraphql = - !project.hasProperty("excludeGraphql") && springBoot2SupportsOptionalIntegrations() -val includeKafka = !project.hasProperty("excludeKafka") && springBoot2SupportsOptionalIntegrations() - tasks.withType().configureEach { kotlin { compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") @@ -48,9 +36,7 @@ dependencies { implementation(libs.springboot.starter) implementation(libs.springboot.starter.actuator) implementation(libs.springboot.starter.aop) - if (includeGraphql) { - implementation(libs.springboot.starter.graphql) - } + implementation(libs.springboot.starter.graphql) implementation(libs.springboot.starter.jdbc) implementation(libs.springboot.starter.quartz) implementation(libs.springboot.starter.security) @@ -62,17 +48,13 @@ dependencies { implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) implementation(projects.sentrySpringBootStarter) implementation(projects.sentryLogback) - if (includeGraphql) { - implementation(projects.sentryGraphql) - } + implementation(projects.sentryGraphql) implementation(projects.sentryQuartz) implementation(projects.sentryAsyncProfiler) implementation(libs.otel) - if (includeKafka) { - implementation(libs.spring.kafka2) - implementation(projects.sentryKafka) - } + implementation(libs.spring.kafka2) + implementation(projects.sentryKafka) // database query tracing implementation(projects.sentryJdbc) @@ -101,19 +83,6 @@ tasks.jar { tasks.startScripts { dependsOn(tasks.shadowJar) } -configure { - main { - if (!includeGraphql) { - java.exclude("**/graphql/**") - resources.exclude("graphql/**") - } - if (!includeKafka) { - java.exclude("**/queues/kafka/**") - resources.exclude("application-kafka.properties") - } - } -} - tasks.register("bootRunWithAgent").configure { group = "application" diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts index 0294f940e9c..d01da9f56c5 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts @@ -25,30 +25,16 @@ group = "io.sentry.sample.spring-boot" version = "0.0.1-SNAPSHOT" -fun springBoot2SupportsGraphql(): Boolean { - val version = libs.versions.springboot2.get().removeSuffix(".RELEASE") - val parts = version.split(".").map { it.toIntOrNull() ?: 0 } - val major = parts.getOrElse(0) { 0 } - val minor = parts.getOrElse(1) { 0 } - return major > 2 || (major == 2 && minor >= 7) -} - -val includeGraphql = !project.hasProperty("excludeGraphql") && springBoot2SupportsGraphql() - dependencies { implementation(platform(libs.springboot2.bom)) implementation(libs.springboot.starter.actuator) - if (includeGraphql) { - implementation(libs.springboot.starter.graphql) - } + implementation(libs.springboot.starter.graphql) implementation(libs.springboot.starter.webflux) implementation(Config.Libs.kotlinReflect) implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) implementation(projects.sentrySpringBootStarter) implementation(projects.sentryLogback) - if (includeGraphql) { - implementation(projects.sentryGraphql) - } + implementation(projects.sentryGraphql) implementation(projects.sentryAsyncProfiler) } @@ -74,15 +60,6 @@ tasks.jar { tasks.startScripts { dependsOn(tasks.shadowJar) } -configure { - main { - if (!includeGraphql) { - java.exclude("**/graphql/**") - resources.exclude("graphql/**") - } - } -} - tasks.withType().configureEach { kotlin { compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") diff --git a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts index 15b90447529..b4f8bc105ec 100644 --- a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts @@ -25,18 +25,6 @@ group = "io.sentry.sample.spring-boot" version = "0.0.1-SNAPSHOT" -fun springBoot2SupportsOptionalIntegrations(): Boolean { - val version = libs.versions.springboot2.get().removeSuffix(".RELEASE") - val parts = version.split(".").map { it.toIntOrNull() ?: 0 } - val major = parts.getOrElse(0) { 0 } - val minor = parts.getOrElse(1) { 0 } - return major > 2 || (major == 2 && minor >= 7) -} - -val includeGraphql = - !project.hasProperty("excludeGraphql") && springBoot2SupportsOptionalIntegrations() -val includeKafka = !project.hasProperty("excludeKafka") && springBoot2SupportsOptionalIntegrations() - tasks.withType().configureEach { kotlin { compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") @@ -48,9 +36,7 @@ dependencies { implementation(libs.springboot.starter) implementation(libs.springboot.starter.actuator) implementation(libs.springboot.starter.aop) - if (includeGraphql) { - implementation(libs.springboot.starter.graphql) - } + implementation(libs.springboot.starter.graphql) implementation(libs.springboot.starter.jdbc) implementation(libs.springboot.starter.quartz) implementation(libs.springboot.starter.security) @@ -60,18 +46,14 @@ dependencies { implementation(libs.springboot.starter.websocket) implementation(libs.caffeine) - if (includeKafka) { - implementation(libs.spring.kafka2) - implementation(projects.sentryKafka) - } + implementation(libs.spring.kafka2) + implementation(projects.sentryKafka) implementation(Config.Libs.aspectj) implementation(Config.Libs.kotlinReflect) implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) implementation(projects.sentrySpringBootStarter) implementation(projects.sentryLogback) - if (includeGraphql) { - implementation(projects.sentryGraphql) - } + implementation(projects.sentryGraphql) implementation(projects.sentryQuartz) implementation(projects.sentryAsyncProfiler) @@ -108,16 +90,3 @@ tasks.jar { } tasks.startScripts { dependsOn(tasks.shadowJar) } - -configure { - main { - if (!includeGraphql) { - java.exclude("**/graphql/**") - resources.exclude("graphql/**") - } - if (!includeKafka) { - java.exclude("**/queues/kafka/**") - resources.exclude("application-kafka.properties") - } - } -} diff --git a/sentry-spring-boot-4/src/integrationTest/kotlin/io/sentry/spring/boot4/integrationtest/PersonIntegrationTest.kt b/sentry-spring-boot-4/src/integrationTest/kotlin/io/sentry/spring/boot4/integrationtest/PersonIntegrationTest.kt index 735ce890e3f..30543274d5d 100644 --- a/sentry-spring-boot-4/src/integrationTest/kotlin/io/sentry/spring/boot4/integrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot-4/src/integrationTest/kotlin/io/sentry/spring/boot4/integrationtest/PersonIntegrationTest.kt @@ -19,24 +19,32 @@ class PersonIntegrationTest : IntegrationTest() { @Test fun `failed request reports errors, the transaction and logs`() { - val response = restTemplate.getForEntity("/person/1", String::class.java) + val response = restTemplate.getForEntity("/person/11", String::class.java) assertThat(response.statusCode.value()).isEqualTo(500) Sentry.flush(5000) val logged = - server.awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=1" }.event()!! + server + .awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=11" } + .event()!! assertThat(logged.hasFlag("my-feature-flag", true)).isTrue() val thrown = server .awaitEnvelope { - it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("my-feature-flag", true)).isTrue() val transaction = - server.awaitEnvelope { it.transaction()?.transaction == "GET /person/{id}" }.transaction()!! + server + .awaitEnvelope { + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true + } + .transaction()!! assertThat( transaction.hasOp( "http.server", diff --git a/sentry-spring-boot-4/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot4/otelagentintegrationtest/AgentApp.kt b/sentry-spring-boot-4/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot4/otelagentintegrationtest/AgentApp.kt index 82961625b3d..1ee54ed9795 100644 --- a/sentry-spring-boot-4/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot4/otelagentintegrationtest/AgentApp.kt +++ b/sentry-spring-boot-4/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot4/otelagentintegrationtest/AgentApp.kt @@ -2,6 +2,7 @@ package io.sentry.spring.boot4.otelagentintegrationtest import io.sentry.test.ChildJvmApp import io.sentry.test.MockSentryServer +import io.sentry.test.requiredTestProperty import org.springframework.boot.restclient.RestTemplateBuilder import org.springframework.boot.resttestclient.TestRestTemplate import org.springframework.kafka.test.EmbeddedKafkaKraftBroker @@ -15,7 +16,7 @@ import org.springframework.kafka.test.EmbeddedKafkaKraftBroker * Spring Boot starter. */ object AgentApp { - val autoInit = System.getProperty("sentry.test.agentAutoInit").toBoolean() + val autoInit = requiredTestProperty("sentry.test.agentAutoInit").toBoolean() val server = MockSentryServer() @@ -25,8 +26,8 @@ object AgentApp { private val app = ChildJvmApp.start( mainClass = "io.sentry.spring.boot4.agentfixture.AgentFixtureApplicationKt", - classpath = System.getProperty("sentry.test.appClasspath"), - jvmArgs = listOf("-javaagent:${System.getProperty("sentry.test.agentJar")}"), + classpath = requiredTestProperty("sentry.test.appClasspath"), + jvmArgs = listOf("-javaagent:${requiredTestProperty("sentry.test.agentJar")}"), // Environment variables, because the agent doesn't read Spring Boot's properties. env = mapOf( diff --git a/sentry-spring-boot-4/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot4/otelagentintegrationtest/PersonIntegrationTest.kt b/sentry-spring-boot-4/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot4/otelagentintegrationtest/PersonIntegrationTest.kt index 4ad2d9f2b85..09d7aecda14 100644 --- a/sentry-spring-boot-4/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot4/otelagentintegrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot-4/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot4/otelagentintegrationtest/PersonIntegrationTest.kt @@ -22,23 +22,27 @@ class PersonIntegrationTest : IntegrationTest() { @Test fun `failed request reports errors, spans with their flags, and logs`() { - val response = restTemplate.getForEntity("/person/1", String::class.java) + val response = restTemplate.getForEntity("/person/11", String::class.java) assertThat(response.statusCode.value()).isEqualTo(500) val logged = - server.awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=1" }.event()!! + server + .awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=11" } + .event()!! assertThat(logged.hasFlag("my-feature-flag", true)).isTrue() val thrown = server .awaitEnvelope { - it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("my-feature-flag", true)).isTrue() val transactionEnvelope = server.awaitEnvelope { - it.transaction()?.transaction == "GET /person/{id}" + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true } // Whichever initializes Sentry names the SDK, which shows that each mode takes its own path. assertThat(transactionEnvelope.header.sdkVersion!!.name) diff --git a/sentry-spring-boot-4/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot4/otelnoagentintegrationtest/PersonIntegrationTest.kt b/sentry-spring-boot-4/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot4/otelnoagentintegrationtest/PersonIntegrationTest.kt index 036a22717e3..6e0359c459b 100644 --- a/sentry-spring-boot-4/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot4/otelnoagentintegrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot-4/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot4/otelnoagentintegrationtest/PersonIntegrationTest.kt @@ -23,24 +23,32 @@ class PersonIntegrationTest : IntegrationTest() { @Test fun `failed request reports errors, nested spans with their flags, and logs`() { - val response = restTemplate.getForEntity("/person/1", String::class.java) + val response = restTemplate.getForEntity("/person/11", String::class.java) assertThat(response.statusCode.value()).isEqualTo(500) Sentry.flush(5000) val logged = - server.awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=1" }.event()!! + server + .awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=11" } + .event()!! assertThat(logged.hasFlag("inner-feature-flag", true)).isTrue() val thrown = server .awaitEnvelope { - it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("inner-feature-flag", true)).isTrue() val transaction = - server.awaitEnvelope { it.transaction()?.transaction == "GET /person/{id}" }.transaction()!! + server + .awaitEnvelope { + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true + } + .transaction()!! assertThat(transaction.hasOp("http.server")).isTrue() assertThat( transaction.hasSpanWithOp( diff --git a/sentry-spring-boot-4/src/integrationTestOtlp/kotlin/io/sentry/spring/boot4/otlpintegrationtest/PersonIntegrationTest.kt b/sentry-spring-boot-4/src/integrationTestOtlp/kotlin/io/sentry/spring/boot4/otlpintegrationtest/PersonIntegrationTest.kt index 3c0e5ab9feb..50b1c660bb0 100644 --- a/sentry-spring-boot-4/src/integrationTestOtlp/kotlin/io/sentry/spring/boot4/otlpintegrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot-4/src/integrationTestOtlp/kotlin/io/sentry/spring/boot4/otlpintegrationtest/PersonIntegrationTest.kt @@ -48,24 +48,32 @@ class PersonIntegrationTest : IntegrationTest() { @Test fun `failed request reports errors, the transaction and logs`() { - val response = restTemplate.getForEntity("/person/1", String::class.java) + val response = restTemplate.getForEntity("/person/11", String::class.java) assertThat(response.statusCode.value()).isEqualTo(500) Sentry.flush(5000) val logged = - server.awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=1" }.event()!! + server + .awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=11" } + .event()!! assertThat(logged.hasFlag("my-feature-flag", true)).isTrue() val thrown = server .awaitEnvelope { - it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("my-feature-flag", true)).isTrue() val transaction = - server.awaitEnvelope { it.transaction()?.transaction == "GET /person/{id}" }.transaction()!! + server + .awaitEnvelope { + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true + } + .transaction()!! assertThat( transaction.hasOp( "http.server", diff --git a/sentry-spring-boot-jakarta/src/integrationTest/kotlin/io/sentry/spring/boot/jakarta/integrationtest/PersonIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTest/kotlin/io/sentry/spring/boot/jakarta/integrationtest/PersonIntegrationTest.kt index a0838b6fee2..3f8ba722099 100644 --- a/sentry-spring-boot-jakarta/src/integrationTest/kotlin/io/sentry/spring/boot/jakarta/integrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot-jakarta/src/integrationTest/kotlin/io/sentry/spring/boot/jakarta/integrationtest/PersonIntegrationTest.kt @@ -19,24 +19,32 @@ class PersonIntegrationTest : IntegrationTest() { @Test fun `failed request reports errors, the transaction and logs`() { - val response = restTemplate.getForEntity("/person/1", String::class.java) + val response = restTemplate.getForEntity("/person/11", String::class.java) assertThat(response.statusCode.value()).isEqualTo(500) Sentry.flush(5000) val logged = - server.awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=1" }.event()!! + server + .awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=11" } + .event()!! assertThat(logged.hasFlag("my-feature-flag", true)).isTrue() val thrown = server .awaitEnvelope { - it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("my-feature-flag", true)).isTrue() val transaction = - server.awaitEnvelope { it.transaction()?.transaction == "GET /person/{id}" }.transaction()!! + server + .awaitEnvelope { + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true + } + .transaction()!! assertThat( transaction.hasOp( "http.server", diff --git a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/AgentApp.kt b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/AgentApp.kt index 9f9b0fedb98..c70f8df301a 100644 --- a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/AgentApp.kt +++ b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/AgentApp.kt @@ -2,6 +2,7 @@ package io.sentry.spring.boot.jakarta.otelagentintegrationtest import io.sentry.test.ChildJvmApp import io.sentry.test.MockSentryServer +import io.sentry.test.requiredTestProperty import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.boot.web.client.RestTemplateBuilder import org.springframework.kafka.test.EmbeddedKafkaKraftBroker @@ -15,7 +16,7 @@ import org.springframework.kafka.test.EmbeddedKafkaKraftBroker * Spring Boot starter. */ object AgentApp { - val autoInit = System.getProperty("sentry.test.agentAutoInit").toBoolean() + val autoInit = requiredTestProperty("sentry.test.agentAutoInit").toBoolean() val server = MockSentryServer() @@ -25,8 +26,8 @@ object AgentApp { private val app = ChildJvmApp.start( mainClass = "io.sentry.spring.boot.jakarta.agentfixture.AgentFixtureApplicationKt", - classpath = System.getProperty("sentry.test.appClasspath"), - jvmArgs = listOf("-javaagent:${System.getProperty("sentry.test.agentJar")}"), + classpath = requiredTestProperty("sentry.test.appClasspath"), + jvmArgs = listOf("-javaagent:${requiredTestProperty("sentry.test.agentJar")}"), // Environment variables, because the agent doesn't read Spring Boot's properties. env = mapOf( diff --git a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/PersonIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/PersonIntegrationTest.kt index eb76ffbf21a..62b2012163a 100644 --- a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/PersonIntegrationTest.kt @@ -22,23 +22,27 @@ class PersonIntegrationTest : IntegrationTest() { @Test fun `failed request reports errors, spans with their flags, and logs`() { - val response = restTemplate.getForEntity("/person/1", String::class.java) + val response = restTemplate.getForEntity("/person/11", String::class.java) assertThat(response.statusCode.value()).isEqualTo(500) val logged = - server.awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=1" }.event()!! + server + .awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=11" } + .event()!! assertThat(logged.hasFlag("my-feature-flag", true)).isTrue() val thrown = server .awaitEnvelope { - it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("my-feature-flag", true)).isTrue() val transactionEnvelope = server.awaitEnvelope { - it.transaction()?.transaction == "GET /person/{id}" + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true } // Whichever initializes Sentry names the SDK, which shows that each mode takes its own path. assertThat(transactionEnvelope.header.sdkVersion!!.name) diff --git a/sentry-spring-boot-jakarta/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/jakarta/otelnoagentintegrationtest/PersonIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/jakarta/otelnoagentintegrationtest/PersonIntegrationTest.kt index 25ead319e9c..e2ca6eb7b56 100644 --- a/sentry-spring-boot-jakarta/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/jakarta/otelnoagentintegrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot-jakarta/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/jakarta/otelnoagentintegrationtest/PersonIntegrationTest.kt @@ -23,24 +23,32 @@ class PersonIntegrationTest : IntegrationTest() { @Test fun `failed request reports errors, nested spans with their flags, and logs`() { - val response = restTemplate.getForEntity("/person/1", String::class.java) + val response = restTemplate.getForEntity("/person/11", String::class.java) assertThat(response.statusCode.value()).isEqualTo(500) Sentry.flush(5000) val logged = - server.awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=1" }.event()!! + server + .awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=11" } + .event()!! assertThat(logged.hasFlag("inner-feature-flag", true)).isTrue() val thrown = server .awaitEnvelope { - it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("inner-feature-flag", true)).isTrue() val transaction = - server.awaitEnvelope { it.transaction()?.transaction == "GET /person/{id}" }.transaction()!! + server + .awaitEnvelope { + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true + } + .transaction()!! assertThat(transaction.hasOp("http.server")).isTrue() assertThat( transaction.hasSpanWithOp( diff --git a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/IntegrationTest.kt b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/IntegrationTest.kt index 4717ff9ac38..a83f50a73f3 100644 --- a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/IntegrationTest.kt +++ b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/IntegrationTest.kt @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.kafka.test.context.EmbeddedKafka import org.springframework.test.context.DynamicPropertyRegistry import org.springframework.test.context.DynamicPropertySource import org.springframework.test.context.junit4.SpringRunner @@ -14,11 +15,14 @@ import org.springframework.test.context.junit4.SpringRunner /** * Drives [IntegrationTestApplication] over HTTP and asserts what reaches [server]. * - * Subclasses share one application context and one server, so the app starts once per suite. - * Envelopes that arrive late can outlive [server]'s reset, so tests match on values only they - * produce. + * Subclasses share one application context, one server and one Kafka broker, so each starts once + * per suite. Envelopes that arrive late can outlive [server]'s reset, so tests match on values only + * they produce. */ @RunWith(SpringRunner::class) +// An in-JVM broker, so no Docker is needed. As part of the context, it starts before the app's +// Kafka listener connects and stops after it. +@EmbeddedKafka(topics = [KAFKA_TOPIC], bootstrapServersProperty = "spring.kafka.bootstrap-servers") @SpringBootTest( classes = [IntegrationTestApplication::class], webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @@ -27,14 +31,14 @@ import org.springframework.test.context.junit4.SpringRunner "sentry.traces-sample-rate=1.0", "sentry.clients.ok-http-enabled=true", "sentry.enable-cache-tracing=true", + "sentry.enable-queue-tracing=true", "sentry.logs.enabled=true", "sentry.profile-session-sample-rate=1.0", "sentry.profile-lifecycle=TRACE", "sentry.profiling-traces-dir-path=\${java.io.tmpdir}/sentry-integration-test/profiling-traces", - // A database per context, because KafkaIntegrationTest runs a second one that would - // otherwise find the first one's tables. - "spring.datasource.url=jdbc:p6spy:hsqldb:mem:\${random.uuid}", + "spring.datasource.url=jdbc:p6spy:hsqldb:mem:testdb", "spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver", + "spring.kafka.consumer.auto-offset-reset=earliest", ], ) abstract class IntegrationTest { diff --git a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaController.kt b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaController.kt index 16138098859..063724a044c 100644 --- a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaController.kt +++ b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/KafkaController.kt @@ -1,6 +1,5 @@ package io.sentry.spring.boot.integrationtest -import org.springframework.context.annotation.Profile import org.springframework.kafka.annotation.KafkaListener import org.springframework.kafka.core.KafkaTemplate import org.springframework.stereotype.Component @@ -11,10 +10,7 @@ import org.springframework.web.bind.annotation.RestController const val KAFKA_TOPIC = "sentry-topic" -// The beans belong to a profile, so only KafkaIntegrationTest's context needs a broker. - @RestController -@Profile("kafka") @RequestMapping("/kafka") open class KafkaController(private val kafkaTemplate: KafkaTemplate) { @GetMapping("/produce") @@ -25,7 +21,6 @@ open class KafkaController(private val kafkaTemplate: KafkaTemplate e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("my-feature-flag", true)).isTrue() val transaction = - server.awaitEnvelope { it.transaction()?.transaction == "GET /person/{id}" }.transaction()!! + server + .awaitEnvelope { + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true + } + .transaction()!! assertThat( transaction.hasOp( "http.server", diff --git a/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/AgentApp.kt b/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/AgentApp.kt index 9f936920b1d..a6bd0bb35ff 100644 --- a/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/AgentApp.kt +++ b/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/AgentApp.kt @@ -2,6 +2,7 @@ package io.sentry.spring.boot.otelagentintegrationtest import io.sentry.test.ChildJvmApp import io.sentry.test.MockSentryServer +import io.sentry.test.requiredTestProperty import java.io.Closeable import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.boot.web.client.RestTemplateBuilder @@ -19,8 +20,8 @@ class AgentApp(args: List = emptyList()) : Closeable { private val app = ChildJvmApp.start( mainClass = "io.sentry.spring.boot.agentfixture.AgentFixtureApplicationKt", - classpath = System.getProperty("sentry.test.appClasspath"), - jvmArgs = listOf("-javaagent:${System.getProperty("sentry.test.agentJar")}"), + classpath = requiredTestProperty("sentry.test.appClasspath"), + jvmArgs = listOf("-javaagent:${requiredTestProperty("sentry.test.agentJar")}"), // Environment variables, because the agent doesn't read Spring Boot's properties. env = mapOf( @@ -58,7 +59,7 @@ class AgentApp(args: List = emptyList()) : Closeable { } companion object { - val autoInit = System.getProperty("sentry.test.agentAutoInit").toBoolean() + val autoInit = requiredTestProperty("sentry.test.agentAutoInit").toBoolean() /** * The app most test classes share. It takes seconds to start, so each test JVM starts it once diff --git a/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/PersonIntegrationTest.kt b/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/PersonIntegrationTest.kt index 42422c109ca..53a52f77412 100644 --- a/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/otelagentintegrationtest/PersonIntegrationTest.kt @@ -22,23 +22,27 @@ class PersonIntegrationTest : IntegrationTest() { @Test fun `failed request reports errors, spans with their flags, and logs`() { - val response = restTemplate.getForEntity("/person/1", String::class.java) + val response = restTemplate.getForEntity("/person/11", String::class.java) assertThat(response.statusCode.value()).isEqualTo(500) val logged = - server.awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=1" }.event()!! + server + .awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=11" } + .event()!! assertThat(logged.hasFlag("my-feature-flag", true)).isTrue() val thrown = server .awaitEnvelope { - it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("my-feature-flag", true)).isTrue() val transactionEnvelope = server.awaitEnvelope { - it.transaction()?.transaction == "GET /person/{id}" + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true } // Whichever initializes Sentry names the SDK, which shows that each mode takes its own path. assertThat(transactionEnvelope.header.sdkVersion!!.name) diff --git a/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/PersonIntegrationTest.kt b/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/PersonIntegrationTest.kt index 8e97eba4244..6e9b1c1038b 100644 --- a/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot/src/integrationTestOtelNoAgent/kotlin/io/sentry/spring/boot/otelnoagentintegrationtest/PersonIntegrationTest.kt @@ -23,24 +23,32 @@ class PersonIntegrationTest : IntegrationTest() { @Test fun `failed request reports errors, nested spans with their flags, and logs`() { - val response = restTemplate.getForEntity("/person/1", String::class.java) + val response = restTemplate.getForEntity("/person/11", String::class.java) assertThat(response.statusCode.value()).isEqualTo(500) Sentry.flush(5000) val logged = - server.awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=1" }.event()!! + server + .awaitEnvelope { it.event()?.message?.formatted == "Trying person with id=11" } + .event()!! assertThat(logged.hasFlag("inner-feature-flag", true)).isTrue() val thrown = server .awaitEnvelope { - it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=1]" } == true + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true } .event()!! assertThat(thrown.hasFlag("inner-feature-flag", true)).isTrue() val transaction = - server.awaitEnvelope { it.transaction()?.transaction == "GET /person/{id}" }.transaction()!! + server + .awaitEnvelope { + it.transaction()?.let { t -> + t.transaction == "GET /person/{id}" && t.request?.url?.endsWith("/person/11") == true + } == true + } + .transaction()!! assertThat(transaction.hasOp("http.server")).isTrue() assertThat( transaction.hasSpanWithOp( diff --git a/sentry-test-support/src/main/kotlin/io/sentry/test/ChildJvmApp.kt b/sentry-test-support/src/main/kotlin/io/sentry/test/ChildJvmApp.kt index 21eeda7239b..eaf737043cb 100644 --- a/sentry-test-support/src/main/kotlin/io/sentry/test/ChildJvmApp.kt +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/ChildJvmApp.kt @@ -26,12 +26,16 @@ class ChildJvmApp private constructor(private val process: Process, private val fun awaitPort(timeoutSeconds: Long = 60): Int { val deadline = Deadline.after(JavaMonotonicTicker.getInstance(), timeoutSeconds, TimeUnit.SECONDS) + var previous: Int? = null while (true) { - // The app may not have finished writing the file yet, in which case it doesn't parse. + // The app may not have finished writing the file yet, and a partly written port such as "54" + // of "54321" still parses. Neither Spring Boot nor the fixtures write it atomically, so only + // a value that two reads agree on counts. val port = portFile.takeIf { it.exists() }?.readText()?.trim()?.toIntOrNull() - if (port != null) { + if (port != null && port == previous) { return port } + previous = port check(process.isAlive) { "App exited with ${process.exitValue()}" } check(!deadline.hasPassed()) { "App did not start within ${timeoutSeconds}s" } Thread.sleep(100) @@ -82,3 +86,9 @@ class ChildJvmApp private constructor(private val process: Process, private val } } } + +/** Returns a system property that the Gradle test task sets, failing clearly when it isn't set. */ +fun requiredTestProperty(name: String): String = + requireNotNull(System.getProperty(name)) { + "System property $name is not set. Run the test through its Gradle task, which sets it." + } diff --git a/sentry-test-support/src/main/kotlin/io/sentry/test/MockSentryServer.kt b/sentry-test-support/src/main/kotlin/io/sentry/test/MockSentryServer.kt index 158d282e388..0275036526a 100644 --- a/sentry-test-support/src/main/kotlin/io/sentry/test/MockSentryServer.kt +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/MockSentryServer.kt @@ -27,6 +27,9 @@ import java.util.zip.GZIPInputStream class MockSentryServer(private val beforeEachCheck: () -> Unit = {}) : Closeable { private val serializer = JsonSerializer(SentryOptions.empty()) private val received = CopyOnWriteArrayList() + // Rethrown while waiting, so an envelope the server can't read fails the test with its cause + // rather than looking like an envelope that never arrived. + private val receiveErrors = CopyOnWriteArrayList() private val server = HttpServer.create(InetSocketAddress(InetAddress.getByName(HOST), 0), 0).apply { createContext("/api/") { exchange -> receive(exchange) } @@ -47,6 +50,7 @@ class MockSentryServer(private val beforeEachCheck: () -> Unit = {}) : Closeable Deadline.after(JavaMonotonicTicker.getInstance(), timeoutMillis, TimeUnit.MILLISECONDS) while (true) { beforeEachCheck() + throwReceiveError() received.firstOrNull(predicate)?.let { return it } @@ -69,6 +73,7 @@ class MockSentryServer(private val beforeEachCheck: () -> Unit = {}) : Closeable val deadline = Deadline.after(JavaMonotonicTicker.getInstance(), millis, TimeUnit.MILLISECONDS) while (true) { beforeEachCheck() + throwReceiveError() if (received.any(predicate)) { throw AssertionError("Received an envelope that should not have been sent") } @@ -81,6 +86,7 @@ class MockSentryServer(private val beforeEachCheck: () -> Unit = {}) : Closeable fun reset() { received.clear() + receiveErrors.clear() } override fun close() { @@ -95,13 +101,23 @@ class MockSentryServer(private val beforeEachCheck: () -> Unit = {}) : Closeable } else { exchange.requestBody } - serializer.deserializeEnvelope(body)?.let { received.add(it) } + val envelope = + serializer.deserializeEnvelope(body) + ?: throw IllegalStateException("Received a body that is not an envelope") + received.add(envelope) exchange.sendResponseHeaders(200, -1) + } catch (e: Exception) { + receiveErrors.add(e) + exchange.sendResponseHeaders(500, -1) } finally { exchange.close() } } + private fun throwReceiveError() { + receiveErrors.firstOrNull()?.let { throw AssertionError("Could not read an envelope", it) } + } + private companion object { // An IP rather than "localhost", which can resolve to IPv4 for the server and IPv6 for the SDK. const val HOST = "127.0.0.1" diff --git a/sentry-test-support/src/test/kotlin/io/sentry/test/MockSentryServerTest.kt b/sentry-test-support/src/test/kotlin/io/sentry/test/MockSentryServerTest.kt index 0f40f27e9af..128c359622b 100644 --- a/sentry-test-support/src/test/kotlin/io/sentry/test/MockSentryServerTest.kt +++ b/sentry-test-support/src/test/kotlin/io/sentry/test/MockSentryServerTest.kt @@ -3,6 +3,8 @@ package io.sentry.test import com.google.common.truth.Truth.assertThat import io.sentry.Sentry import io.sentry.SentryItemType +import java.net.HttpURLConnection +import java.net.URI import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertFailsWith @@ -64,4 +66,17 @@ class MockSentryServerTest { server.assertNoEnvelope(millis = 5000) { it.header.eventId == eventId } } } + + @Test + fun `awaitEnvelope fails with the cause when a body is not an envelope`() { + val url = URI(server.dsn.replace("key@", "")).resolve("/api/0/envelope/").toURL() + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "POST" + connection.doOutput = true + connection.outputStream.use { it.write("not an envelope".toByteArray()) } + assertThat(connection.responseCode).isEqualTo(500) + + val error = assertFailsWith { server.awaitEnvelope { true } } + assertThat(error).hasCauseThat().isNotNull() + } }