From e7c763db428c98451c0c1917b5c5fc7dfd232599 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 20 Oct 2026 19:20:53 +0200 Subject: [PATCH] test(spring): Move the Spring jakarta WAR sample's tests into the module Adds an integrationTest suite to sentry-spring-jakarta that runs a plain Spring MVC app, set up by an AbstractAnnotationConfigDispatcherServletInitializer as in a WAR, in an embedded Tomcat against MockSentryServer. Tomcat runs every servlet container initializer the classpath registers, as it does for a WAR, and Spring Boot stays off the classpath, so Sentry's initializer adds SentrySpringFilter the way it does in a real deployment. The suite ports the sample's Person and Metrics tests and also checks that the error carries its request. The profiler rejects a traces path with a dot anywhere in its absolute form, so the suite puts it in the temp dir. The sample has no system tests left, so it comes off the system-test runner, the backend workflow and the Spring Boot 3 matrix. That was the matrix's last runner step, so it drops the Python setup and the runner's logs too. integrationTests includes the suite. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/spring-boot-3-matrix.yml | 29 +------ .github/workflows/system-tests-backend.yml | 3 - build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + .../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-jakarta/build.gradle.kts | 24 ++++++ .../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 - 16 files changed, 318 insertions(+), 208 deletions(-) delete mode 100644 sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/DummyTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt create mode 100644 sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/IntegrationTest.kt create mode 100644 sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/IntegrationTestApplication.kt create mode 100644 sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/MetricController.kt create mode 100644 sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/MetricsIntegrationTest.kt create mode 100644 sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/PersonIntegrationTest.kt create mode 100644 sentry-spring-jakarta/src/integrationTest/resources/sentry.properties diff --git a/.github/workflows/spring-boot-3-matrix.yml b/.github/workflows/spring-boot-3-matrix.yml index bf7d92b827a..f698683cdc6 100644 --- a/.github/workflows/spring-boot-3-matrix.yml +++ b/.github/workflows/spring-boot-3-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: @@ -68,15 +57,10 @@ jobs: perl -0pi -e 'BEGIN { $v = shift } s/^springboot3[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot3 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml echo "Updated Spring Boot 3.x version to $springboot_version" - - name: Build sample artifacts - run: | - ./gradlew \ - :sentry-samples:sentry-samples-spring-jakarta:war \ - :sentry-samples:sentry-samples-spring-jakarta:testClasses - - - name: Test sentry-spring-boot-jakarta integration suites + - name: Test integration suites run: | ./gradlew \ + :sentry-spring-jakarta:integrationTest \ :sentry-spring-boot-jakarta:integrationTest \ :sentry-spring-boot-jakarta:integrationTestWebflux \ :sentry-spring-boot-jakarta:integrationTestOtelNoAgent \ @@ -84,13 +68,6 @@ jobs: :sentry-spring-boot-jakarta:integrationTestOtelAgent \ :sentry-spring-boot-jakarta:integrationTestOtelAgentWithoutAutoInit - - name: Test sentry-samples-spring-jakarta - run: | - python3 test/system-test-runner.py test \ - --module "sentry-samples-spring-jakarta" \ - --agent false \ - --auto-init "true" - - name: Upload test results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 @@ -99,8 +76,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 c067dd63092..bece6231f97 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -46,9 +46,6 @@ jobs: - sample: "sentry-samples-spring-7" agent: "false" agent-auto-init: "true" - - sample: "sentry-samples-spring-jakarta" - agent: "false" - agent-auto-init: "true" - sample: "sentry-samples-spring" agent: "false" agent-auto-init: "true" diff --git a/build.gradle.kts b/build.gradle.kts index aa28758edf5..b2c13937efd 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-jakarta:integrationTest", ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d18407753ba..248e99685bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -256,6 +256,8 @@ tomcat-catalina = { module = "org.apache.tomcat:tomcat-catalina", version = "9.0 tomcat-embed-jasper = { module = "org.apache.tomcat.embed:tomcat-embed-jasper", version = "9.0.108" } tomcat-catalina-jakarta = { module = "org.apache.tomcat:tomcat-catalina", version = "11.0.22" } tomcat-embed-jasper-jakarta = { module = "org.apache.tomcat.embed:tomcat-embed-jasper", version = "11.0.22" } +# The version comes from the Spring Boot BOM of whichever Spring line uses it. +tomcat-embed-core = { module = "org.apache.tomcat.embed:tomcat-embed-core" } # test libraries androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version = "1.4.1" } diff --git a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts index 022b9fbb0c6..3a159bb6e11 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts @@ -7,7 +7,6 @@ plugins { alias(libs.plugins.kotlin.spring) id("war") alias(libs.plugins.gretty) - id("io.sentry.systemtest") } application { mainClass.set("io.sentry.samples.spring.jakarta.Main") } @@ -19,9 +18,6 @@ group = "io.sentry.sample.spring-jakarta" 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("org.springframework.boot:spring-boot-dependencies:${libs.versions.springboot3.get()}") @@ -48,12 +44,6 @@ dependencies { implementation(libs.tomcat.catalina.jakarta) implementation(libs.tomcat.embed.jasper.jakarta) - - testImplementation(projects.sentrySystemTestSupport) - testImplementation(libs.kotlin.test.junit) - testImplementation(libs.springboot3.starter.test) { - exclude(group = "org.junit.vintage", module = "junit-vintage-engine") - } } tasks.withType().configureEach { @@ -61,22 +51,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-jakarta/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/DummyTest.kt deleted file mode 100644 index 6f762b7e453..00000000000 --- a/sentry-samples/sentry-samples-spring-jakarta/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-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt deleted file mode 100644 index 90e7a28fb36..00000000000 --- a/sentry-samples/sentry-samples-spring-jakarta/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-jakarta-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-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt deleted file mode 100644 index af79ec8dc7e..00000000000 --- a/sentry-samples/sentry-samples-spring-jakarta/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-jakarta-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-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index 138ad1e5f16..7f4f4d722ba 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -107,3 +107,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(SpringBootPlugin.BOM_COORDINATES)) + 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-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/IntegrationTest.kt b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/IntegrationTest.kt new file mode 100644 index 00000000000..4db96cfdfd5 --- /dev/null +++ b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/IntegrationTest.kt @@ -0,0 +1,72 @@ +package io.sentry.spring.jakarta.integrationtest + +import io.sentry.test.MockSentryServer +import jakarta.servlet.ServletContainerInitializer +import java.nio.file.Files +import java.util.ServiceLoader +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-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/IntegrationTestApplication.kt b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/IntegrationTestApplication.kt new file mode 100644 index 00000000000..89da9442f1f --- /dev/null +++ b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/IntegrationTestApplication.kt @@ -0,0 +1,66 @@ +package io.sentry.spring.jakarta.integrationtest + +import io.sentry.Sentry +import io.sentry.spring.jakarta.EnableSentry +import io.sentry.spring.jakarta.tracing.SentryTracingFilter +import jakarta.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-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/MetricController.kt b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/MetricController.kt new file mode 100644 index 00000000000..e6f454fa963 --- /dev/null +++ b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/MetricController.kt @@ -0,0 +1,33 @@ +package io.sentry.spring.jakarta.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-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/MetricsIntegrationTest.kt b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/MetricsIntegrationTest.kt new file mode 100644 index 00000000000..57188f421e7 --- /dev/null +++ b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/MetricsIntegrationTest.kt @@ -0,0 +1,50 @@ +package io.sentry.spring.jakarta.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-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/PersonIntegrationTest.kt b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/PersonIntegrationTest.kt new file mode 100644 index 00000000000..285ea88226e --- /dev/null +++ b/sentry-spring-jakarta/src/integrationTest/kotlin/io/sentry/spring/jakarta/integrationtest/PersonIntegrationTest.kt @@ -0,0 +1,64 @@ +package io.sentry.spring.jakarta.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-jakarta/src/integrationTest/resources/sentry.properties b/sentry-spring-jakarta/src/integrationTest/resources/sentry.properties new file mode 100644 index 00000000000..31e3807a8b2 --- /dev/null +++ b/sentry-spring-jakarta/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 741fbf62ea1..e734ab1aaeb 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -829,7 +829,6 @@ def get_available_modules(self) -> List[ModuleConfig]: return [ ModuleConfig("sentry-samples-spring", "false", "true", "false"), ModuleConfig("sentry-samples-spring-7", "false", "true", "false"), - ModuleConfig("sentry-samples-spring-jakarta", "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"),