From 334a404432d7cf83cd0f71bc5b4838534df2e9a6 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 20 Oct 2026 19:29:19 +0200 Subject: [PATCH] test(spring): Move the Spring WAR sample's tests into the module Adds sentry-spring-jakarta's embedded-Tomcat integrationTest suite to sentry-spring, with the module's packages and javax.servlet. The sample's tests were the jakarta WAR sample's, so the suite is too. Spring Boot 2's BOM picks Spring 5.3 and Tomcat 9. The sample has no system tests left, so it comes off the system-test runner, the backend workflow and the Spring Boot 2 matrix. That was the matrix's last runner step, so it drops the Python setup, the runner's logs and the exclude properties only the samples and the runner read. integrationTests includes the suite. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/spring-boot-2-matrix.yml | 33 +------ .github/workflows/system-tests-backend.yml | 3 - build.gradle.kts | 1 + .../sentry-samples-spring/build.gradle.kts | 29 ------- .../src/test/kotlin/io/sentry/DummyTest.kt | 12 --- .../io/sentry/systemtest/MetricsSystemTest.kt | 51 ----------- .../io/sentry/systemtest/PersonSystemTest.kt | 85 ------------------- sentry-spring/build.gradle.kts | 24 ++++++ .../spring/integrationtest/IntegrationTest.kt | 72 ++++++++++++++++ .../IntegrationTestApplication.kt | 66 ++++++++++++++ .../integrationtest/MetricController.kt | 33 +++++++ .../integrationtest/MetricsIntegrationTest.kt | 50 +++++++++++ .../integrationtest/PersonIntegrationTest.kt | 64 ++++++++++++++ .../resources/sentry.properties | 4 + test/system-test-runner.py | 1 - 15 files changed, 316 insertions(+), 212 deletions(-) delete mode 100644 sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/DummyTest.kt delete mode 100644 sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt delete mode 100644 sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt create mode 100644 sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/IntegrationTest.kt create mode 100644 sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/IntegrationTestApplication.kt create mode 100644 sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/MetricController.kt create mode 100644 sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/MetricsIntegrationTest.kt create mode 100644 sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/PersonIntegrationTest.kt create mode 100644 sentry-spring/src/integrationTest/resources/sentry.properties diff --git a/.github/workflows/spring-boot-2-matrix.yml b/.github/workflows/spring-boot-2-matrix.yml index 30f7c8e7020..edfe9e74fc0 100644 --- a/.github/workflows/spring-boot-2-matrix.yml +++ b/.github/workflows/spring-boot-2-matrix.yml @@ -25,7 +25,6 @@ jobs: name: Spring Boot ${{ matrix.springboot-version }} env: - SENTRY_URL: http://127.0.0.1:8000 GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} steps: @@ -34,16 +33,6 @@ jobs: with: submodules: 'recursive' - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: '3.10' - - - name: Install Python dependencies - run: | - python3 -m pip install --upgrade pip - python3 -m pip install -r requirements.txt - - name: Set up Java uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: @@ -65,35 +54,19 @@ jobs: - name: Update Spring Boot 2.x version run: | springboot_version="${{ matrix.springboot-version }}" - if [[ ! "$springboot_version" =~ ^2\.7\. ]]; then - echo "ORG_GRADLE_PROJECT_excludeGraphql=true" >> "$GITHUB_ENV" - echo "ORG_GRADLE_PROJECT_excludeKafka=true" >> "$GITHUB_ENV" - fi perl -0pi -e 'BEGIN { $v = shift } s/^springboot2[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot2 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml echo "Updated Spring Boot 2.x version to $springboot_version" - - name: Build sample artifacts - run: | - ./gradlew \ - :sentry-samples:sentry-samples-spring:war \ - :sentry-samples:sentry-samples-spring:testClasses - - - name: Test sentry-spring-boot integration suites + - name: Test integration suites run: | ./gradlew \ + :sentry-spring:integrationTest \ :sentry-spring-boot:integrationTest \ :sentry-spring-boot:integrationTestWebflux \ :sentry-spring-boot:integrationTestOtelNoAgent \ :sentry-spring-boot:integrationTestOtelAgent \ :sentry-spring-boot:integrationTestOtelAgentWithoutAutoInit - - name: Test sentry-samples-spring - run: | - python3 test/system-test-runner.py test \ - --module "sentry-samples-spring" \ - --agent false \ - --auto-init "true" - - name: Upload test results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 @@ -102,8 +75,6 @@ jobs: path: | **/build/reports/* **/build/test-results/**/*.xml - sentry-mock-server.txt - spring-server.txt - name: Test Report uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16 diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index 04a3fb56786..2899af56d0e 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -43,9 +43,6 @@ jobs: - sample: "sentry-samples-spring-boot-4-otlp" agent: "false" agent-auto-init: "true" - - sample: "sentry-samples-spring" - agent: "false" - agent-auto-init: "true" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/build.gradle.kts b/build.gradle.kts index 79c0e442c13..44b9ffc7f5e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -240,6 +240,7 @@ tasks.register("integrationTests") { ":sentry-spring-boot-4:integrationTestLog4j2", ":sentry-spring-boot-4:integrationTestOtelAgent", ":sentry-spring-boot-4:integrationTestOtelAgentWithoutAutoInit", + ":sentry-spring:integrationTest", ":sentry-spring-jakarta:integrationTest", ":sentry-spring-7:integrationTest", ) diff --git a/sentry-samples/sentry-samples-spring/build.gradle.kts b/sentry-samples/sentry-samples-spring/build.gradle.kts index 8ae94a2e367..b64fcab92a3 100644 --- a/sentry-samples/sentry-samples-spring/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring/build.gradle.kts @@ -7,7 +7,6 @@ plugins { alias(libs.plugins.kotlin.spring) id("war") alias(libs.plugins.gretty) - id("io.sentry.systemtest") } // Spring scans this app's own bytecode with its repackaged ASM at startup, and the oldest version @@ -29,9 +28,6 @@ group = "io.sentry.sample.spring" version = "0.0.1-SNAPSHOT" -// Apollo 4.x requires coroutines 1.9.0+, override Spring Boot's managed version -extra["kotlin-coroutines.version"] = "1.9.0" - dependencyManagement { imports { mavenBom(libs.springboot2.bom.get().toString()) @@ -58,12 +54,6 @@ dependencies { implementation(libs.tomcat.catalina) implementation(libs.tomcat.embed.jasper) - - testImplementation(projects.sentrySystemTestSupport) - testImplementation(libs.kotlin.test.junit) - testImplementation(libs.springboot.starter.test) { - exclude(group = "org.junit.vintage", module = "junit-vintage-engine") - } } tasks.withType().configureEach { @@ -71,22 +61,3 @@ tasks.withType().configureEach { compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") } } - -tasks.register("systemTest").configure { - group = "verification" - description = "Runs the System tests" - - val test = project.extensions.getByType()["test"] - testClassesDirs = test.output.classesDirs - classpath = test.runtimeClasspath - - maxParallelForks = 1 - - filter { includeTestsMatching("io.sentry.systemtest*") } -} - -tasks.named("test").configure { - require(this is Test) - - filter { excludeTestsMatching("io.sentry.systemtest.*") } -} diff --git a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/DummyTest.kt deleted file mode 100644 index 6f762b7e453..00000000000 --- a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/DummyTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.sentry - -import kotlin.test.Test -import kotlin.test.assertTrue - -class DummyTest { - @Test - fun `the only test`() { - // only needed to have more than 0 tests and not fail the build - assertTrue(true) - } -} diff --git a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt deleted file mode 100644 index 8a5cd29fe6d..00000000000 --- a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package io.sentry.systemtest - -import io.sentry.systemtest.util.TestHelper -import kotlin.test.Test -import kotlin.test.assertEquals -import org.junit.Before - -class MetricsSystemTest { - lateinit var testHelper: TestHelper - - @Before - fun setup() { - testHelper = TestHelper("http://localhost:8080/sentry-samples-spring-0.0.1-SNAPSHOT") - testHelper.reset() - } - - @Test - fun `count metric`() { - val restClient = testHelper.restClient - assertEquals("count metric increased", restClient.getCountMetric()) - assertEquals(200, restClient.lastKnownStatusCode) - - testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) && - testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") && - testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2) - } - } - - @Test - fun `gauge metric`() { - val restClient = testHelper.restClient - assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) - assertEquals(200, restClient.lastKnownStatusCode) - - testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) - } - } - - @Test - fun `distribution metric`() { - val restClient = testHelper.restClient - assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) - assertEquals(200, restClient.lastKnownStatusCode) - - testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) - } - } -} diff --git a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt deleted file mode 100644 index 0565ee5c33e..00000000000 --- a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package io.sentry.systemtest - -import io.sentry.protocol.SentryId -import io.sentry.systemtest.util.TestHelper -import kotlin.test.Test -import kotlin.test.assertEquals -import org.junit.Before - -class PersonSystemTest { - lateinit var testHelper: TestHelper - - @Before - fun setup() { - testHelper = TestHelper("http://localhost:8080/sentry-samples-spring-0.0.1-SNAPSHOT") - testHelper.reset() - } - - @Test - fun `get person fails`() { - val restClient = testHelper.restClient - restClient.getPerson(11L) - assertEquals(500, restClient.lastKnownStatusCode) - - testHelper.ensureErrorReceived { event -> - testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=11]") && - testHelper.doesEventHaveFlag(event, "my-feature-flag", true) - } - - testHelper.ensureTransactionReceived { transaction, envelopeHeader -> - testHelper.doesTransactionHaveOp(transaction, "http.server") - } - - Thread.sleep(10000) - - testHelper.ensureLogsReceived { logs, envelopeHeader -> - testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && - testHelper.doesContainLogWithBody(logs, "error Sentry logging") && - testHelper.doesContainLogWithBody(logs, "hello there world!") && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "warn Sentry logging", - "user.type", - "admin", - ) && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "warn Sentry logging", - "feature.version", - 2, - ) && - testHelper.doesLogWithBodyHaveAttribute(logs, "warn Sentry logging", "debug.enabled", true) - } - } - - @Test - fun `create person works`() { - val restClient = testHelper.restClient - val person = Person("firstA", "lastB") - val returnedPerson = restClient.createPerson(person) - assertEquals(200, restClient.lastKnownStatusCode) - - assertEquals(person.firstName, returnedPerson!!.firstName) - assertEquals(person.lastName, returnedPerson!!.lastName) - } - - @Test - fun `create person starts a profile linked to the transaction`() { - var profilerId: SentryId? = null - val restClient = testHelper.restClient - val person = Person("firstA", "lastB") - val returnedPerson = restClient.createPerson(person) - assertEquals(200, restClient.lastKnownStatusCode) - - assertEquals(person.firstName, returnedPerson!!.firstName) - assertEquals(person.lastName, returnedPerson!!.lastName) - - testHelper.ensureTransactionReceived { transaction, envelopeHeader -> - profilerId = transaction.contexts.profile?.profilerId - transaction.transaction == "POST /person/" - } - testHelper.ensureProfileChunkReceived { profileChunk, envelopeHeader -> - profileChunk.profilerId == profilerId - } - } -} diff --git a/sentry-spring/build.gradle.kts b/sentry-spring/build.gradle.kts index 71eb6d1f15e..c82e57d4fbf 100644 --- a/sentry-spring/build.gradle.kts +++ b/sentry-spring/build.gradle.kts @@ -95,3 +95,27 @@ tasks.jar { ) } } + +testing { + suites { + // Runs a plain Spring MVC app in an embedded Tomcat against MockSentryServer, the way a servlet + // container runs a WAR. Spring Boot isn't on its classpath, because Sentry behaves differently + // with it. + register("integrationTest") { + useJUnit() + dependencies { + implementation(platform(libs.springboot2.bom)) + implementation(project()) + implementation(projects.sentryAsyncProfiler) + implementation(projects.sentryTestSupport) + implementation(Config.Libs.springWeb) + implementation(libs.jackson.databind) + implementation(libs.kotlin.test.junit) + implementation(libs.google.truth) + implementation(libs.tomcat.embed.core) + } + } + } +} + +tasks.named("check") { dependsOn(testing.suites.named("integrationTest")) } diff --git a/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/IntegrationTest.kt b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/IntegrationTest.kt new file mode 100644 index 00000000000..130ce0918d4 --- /dev/null +++ b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/IntegrationTest.kt @@ -0,0 +1,72 @@ +package io.sentry.spring.integrationtest + +import io.sentry.test.MockSentryServer +import java.nio.file.Files +import java.util.ServiceLoader +import javax.servlet.ServletContainerInitializer +import org.apache.catalina.startup.Tomcat +import org.junit.Before +import org.springframework.http.client.ClientHttpResponse +import org.springframework.web.client.DefaultResponseErrorHandler +import org.springframework.web.client.RestTemplate +import org.springframework.web.util.DefaultUriBuilderFactory + +/** + * Drives [AppInitializer]'s app over HTTP and asserts what reaches [server]. + * + * Subclasses share one app and one server. Envelopes that arrive late can outlive [server]'s reset, + * so tests match on values only they produce. + */ +abstract class IntegrationTest { + val server = TomcatApp.server + val restTemplate = TomcatApp.restTemplate + + @Before + fun resetServer() { + server.reset() + } +} + +/** + * The app in an embedded Tomcat, with no Spring Boot on the classpath, so Sentry takes the same + * path as in a WAR. Tomcat starts once per test JVM. + */ +object TomcatApp { + val server = MockSentryServer() + + init { + System.setProperty("sentry.dsn", server.dsn) + // The profiler rejects a traces path with a dot anywhere in it, which the module's absolute + // path can have. + System.setProperty( + "sentry.profiling-traces-dir-path", + "${System.getProperty("java.io.tmpdir")}/sentry-integration-test/profiling-traces", + ) + } + + private val tomcat = + Tomcat().apply { + setBaseDir(Files.createTempDirectory("tomcat").toString()) + setPort(0) + // Creates the default connector. + connector + val context = addContext("", Files.createTempDirectory("docbase").toString()) + // As Tomcat does for a WAR, runs each initializer the classpath registers: Spring's starts + // AppInitializer's app, and Sentry's adds SentrySpringFilter. Only Spring's uses the set. + ServiceLoader.load(ServletContainerInitializer::class.java).forEach { + context.addServletContainerInitializer(it, setOf(AppInitializer::class.java)) + } + start() + } + + val restTemplate = + RestTemplate().apply { + uriTemplateHandler = + DefaultUriBuilderFactory("http://localhost:${tomcat.connector.localPort}") + // Returns error responses rather than throwing them, so tests can check a failed request. + errorHandler = + object : DefaultResponseErrorHandler() { + override fun hasError(response: ClientHttpResponse): Boolean = false + } + } +} diff --git a/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/IntegrationTestApplication.kt b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/IntegrationTestApplication.kt new file mode 100644 index 00000000000..f51d475470c --- /dev/null +++ b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/IntegrationTestApplication.kt @@ -0,0 +1,66 @@ +package io.sentry.spring.integrationtest + +import io.sentry.Sentry +import io.sentry.spring.EnableSentry +import io.sentry.spring.tracing.SentryTracingFilter +import javax.servlet.Filter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.servlet.config.annotation.EnableWebMvc +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer + +// Classes and methods are open because Spring proxies them and this module doesn't apply the +// kotlin-spring plugin. + +/** Sets the app up the way a WAR does, for the servlet container to start. */ +class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { + override fun getRootConfigClasses(): Array> = arrayOf(AppConfig::class.java) + + override fun getServletConfigClasses(): Array> = arrayOf(WebConfig::class.java) + + override fun getServletMappings(): Array = arrayOf("/*") + + // Creates Sentry transactions around incoming requests. + override fun getServletFilters(): Array = arrayOf(SentryTracingFilter()) +} + +// The DSN and the traces path come from system properties, and the other options from +// sentry.properties, all of which Sentry reads as external configuration. +@Configuration @EnableSentry(dsn = "", sendDefaultPii = true) open class AppConfig + +// Declares the controllers rather than scanning for them, which would also pick up AppConfig and +// initialize Sentry a second time in the servlet's context. +@Configuration +@EnableWebMvc +open class WebConfig { + @Bean open fun personController() = PersonController() + + @Bean open fun metricController() = MetricController() +} + +open class Person(var firstName: String? = null, var lastName: String? = null) + +@RestController +@RequestMapping("/person/") +open class PersonController { + /** Reports to Sentry through every API an app uses, then fails the request. */ + @GetMapping("{id}") + open fun person(@PathVariable("id") id: Long): Person { + Sentry.setAttribute("user.type", "admin") + Sentry.setAttribute("feature.version", 2) + Sentry.setAttribute("debug.enabled", true) + Sentry.logger().warn("warn Sentry logging") + Sentry.logger().error("error Sentry logging") + Sentry.logger().info("hello %s %s", "there", "world!") + Sentry.addFeatureFlag("my-feature-flag", true) + throw IllegalArgumentException("Something went wrong [id=$id]") + } + + @PostMapping open fun create(@RequestBody person: Person): Person = person +} diff --git a/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/MetricController.kt b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/MetricController.kt new file mode 100644 index 00000000000..61041bf6d21 --- /dev/null +++ b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/MetricController.kt @@ -0,0 +1,33 @@ +package io.sentry.spring.integrationtest + +import io.sentry.Sentry +import io.sentry.metrics.MetricsUnit +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/metric/") +open class MetricController { + @GetMapping("count") + open fun count(): String { + Sentry.setAttribute("user.type", "admin") + Sentry.setAttribute("feature.version", 2) + Sentry.metrics().count("countMetric") + return "count metric increased" + } + + @GetMapping("gauge/{value}") + open fun gauge(@PathVariable("value") value: Long): String { + Sentry.metrics().gauge("memory.free", value.toDouble(), MetricsUnit.Information.BYTE) + return "gauge metric tracked" + } + + @GetMapping("distribution/{value}") + open fun distribution(@PathVariable("value") value: Long): String { + Sentry.metrics() + .distribution("distributionMetric", value.toDouble(), MetricsUnit.Duration.MILLISECOND) + return "distribution metric tracked" + } +} diff --git a/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/MetricsIntegrationTest.kt b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/MetricsIntegrationTest.kt new file mode 100644 index 00000000000..d2e524a3bb3 --- /dev/null +++ b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/MetricsIntegrationTest.kt @@ -0,0 +1,50 @@ +package io.sentry.spring.integrationtest + +import com.google.common.truth.Truth.assertThat +import io.sentry.Sentry +import io.sentry.SentryMetricsEvents +import io.sentry.test.hasMetric +import io.sentry.test.hasMetricWithAttribute +import io.sentry.test.metrics +import kotlin.test.Test + +class MetricsIntegrationTest : IntegrationTest() { + + @Test + fun `count metric carries the scope's attributes`() { + assertThat(restTemplate.getForObject("/metric/count", String::class.java)) + .isEqualTo("count metric increased") + + val metrics = awaitMetric("countMetric") + assertThat(metrics.hasMetric("countMetric", "counter", 1.0)).isTrue() + assertThat(metrics.hasMetricWithAttribute("countMetric", "user.type", "admin")).isTrue() + assertThat(metrics.hasMetricWithAttribute("countMetric", "feature.version", 2)).isTrue() + } + + @Test + fun `gauge metric`() { + assertThat(restTemplate.getForObject("/metric/gauge/14", String::class.java)) + .isEqualTo("gauge metric tracked") + + assertThat(awaitMetric("memory.free").hasMetric("memory.free", "gauge", 14.0)).isTrue() + } + + @Test + fun `distribution metric`() { + assertThat(restTemplate.getForObject("/metric/distribution/23", String::class.java)) + .isEqualTo("distribution metric tracked") + + assertThat( + awaitMetric("distributionMetric").hasMetric("distributionMetric", "distribution", 23.0) + ) + .isTrue() + } + + /** Flushes first because metrics are batched before they're sent. */ + private fun awaitMetric(name: String): SentryMetricsEvents { + Sentry.flush(5000) + return server + .awaitEnvelope { it.metrics()?.items?.any { metric -> metric.name == name } == true } + .metrics()!! + } +} diff --git a/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/PersonIntegrationTest.kt b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/PersonIntegrationTest.kt new file mode 100644 index 00000000000..9457a5da5dc --- /dev/null +++ b/sentry-spring/src/integrationTest/kotlin/io/sentry/spring/integrationtest/PersonIntegrationTest.kt @@ -0,0 +1,64 @@ +package io.sentry.spring.integrationtest + +import com.google.common.truth.Truth.assertThat +import io.sentry.Sentry +import io.sentry.test.event +import io.sentry.test.hasFlag +import io.sentry.test.hasLogWithAttribute +import io.sentry.test.hasLogWithBody +import io.sentry.test.logs +import io.sentry.test.profileChunk +import io.sentry.test.transaction +import kotlin.test.Test + +class PersonIntegrationTest : IntegrationTest() { + + @Test + fun `failed request reports the error with its request, the transaction and logs`() { + val response = restTemplate.getForEntity("/person/11", String::class.java) + assertThat(response.statusCode.value()).isEqualTo(500) + Sentry.flush(5000) + + val error = + server + .awaitEnvelope { + it.event()?.exceptions?.any { e -> e.value == "Something went wrong [id=11]" } == true + } + .event()!! + assertThat(error.hasFlag("my-feature-flag", true)).isTrue() + // Without Spring Boot, Sentry's servlet container initializer registers SentrySpringFilter, + // which attaches the request. + assertThat(error.request?.url).endsWith("/person/11") + + val transaction = + server.awaitEnvelope { it.transaction()?.transaction == "GET /person/{id}" }.transaction()!! + assertThat(transaction.contexts.trace?.operation).isEqualTo("http.server") + + val logs = + server + .awaitEnvelope { + it.logs()?.items?.any { log -> log.body == "warn Sentry logging" } == true + } + .logs()!! + assertThat(logs.hasLogWithBody("error Sentry logging")).isTrue() + assertThat(logs.hasLogWithBody("hello there world!")).isTrue() + assertThat(logs.hasLogWithAttribute("warn Sentry logging", "user.type", "admin")).isTrue() + assertThat(logs.hasLogWithAttribute("warn Sentry logging", "feature.version", 2)).isTrue() + assertThat(logs.hasLogWithAttribute("warn Sentry logging", "debug.enabled", true)).isTrue() + } + + @Test + fun `creating a person starts a profile linked to the transaction`() { + val response = + restTemplate.postForEntity("/person/", Person("firstA", "lastB"), Person::class.java) + assertThat(response.statusCode.value()).isEqualTo(200) + assertThat(response.body!!.firstName).isEqualTo("firstA") + assertThat(response.body!!.lastName).isEqualTo("lastB") + + val transaction = + server.awaitEnvelope { it.transaction()?.transaction == "POST /person/" }.transaction()!! + val profilerId = transaction.contexts.profile?.profilerId + assertThat(profilerId).isNotNull() + server.awaitEnvelope(timeoutMillis = 30_000) { it.profileChunk()?.profilerId == profilerId } + } +} diff --git a/sentry-spring/src/integrationTest/resources/sentry.properties b/sentry-spring/src/integrationTest/resources/sentry.properties new file mode 100644 index 00000000000..31e3807a8b2 --- /dev/null +++ b/sentry-spring/src/integrationTest/resources/sentry.properties @@ -0,0 +1,4 @@ +traces-sample-rate=1.0 +logs.enabled=true +profile-session-sample-rate=1.0 +profile-lifecycle=TRACE diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 60653fdf608..89a95a42007 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -827,7 +827,6 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, def get_available_modules(self) -> List[ModuleConfig]: """Get list of all available test modules.""" return [ - ModuleConfig("sentry-samples-spring", "false", "true", "false"), ModuleConfig("sentry-samples-spring-boot-4-otlp", "false", "true", "false"), ModuleConfig("sentry-samples-console", "false", "true", "false"), ModuleConfig("sentry-samples-console-otlp", "false", "true", "false"),