Skip to content

Commit 2411eee

Browse files
committed
test(spring-boot): Tighten the Boot 2 WebFlux tracing checks and restore Metrics
Ports the tracing checks and the plain test client from the servlet suite, and runs the WebFlux sample's Metrics tests in the reactive app too, rather than only in the servlet suite. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 00a731e commit 2411eee

4 files changed

Lines changed: 109 additions & 8 deletions

File tree

‎sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/DistributedTracingWebfluxIntegrationTest.kt‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat
44
import io.sentry.SentryEnvelope
55
import io.sentry.protocol.SentryId
66
import io.sentry.test.transaction
7+
import kotlin.random.Random
78
import kotlin.test.Test
89
import org.springframework.http.HttpEntity
910
import org.springframework.http.HttpHeaders
@@ -27,26 +28,26 @@ class DistributedTracingWebfluxIntegrationTest : WebfluxIntegrationTest() {
2728
)
2829
assertThat(status).isEqualTo(500)
2930

30-
awaitTransaction("GET /tracing/{id}", traceId)
31+
val incoming = awaitTransaction("GET /tracing/{id}", traceId).transaction()!!
32+
assertThat(incoming.request?.url).endsWith("/tracing/1")
3133
awaitTransaction("GET /person/{id}", traceId)
3234
}
3335

3436
@Test
3537
fun `unsampled trace sends no transactions`() {
3638
val traceId = SentryId()
39+
// An id only this request uses, because a mishandled header could start a trace of its own.
40+
val id = Random.nextLong(2, Long.MAX_VALUE)
3741
val status =
3842
get(
39-
"/tracing/1",
43+
"/tracing/$id",
4044
sentryTrace = "$traceId-424cffc8f94feeee-0",
4145
baggage =
4246
"sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=false,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET",
4347
)
4448
assertThat(status).isEqualTo(500)
4549

46-
server.assertNoEnvelope {
47-
isTransaction(it, "GET /tracing/{id}", traceId) ||
48-
isTransaction(it, "GET /person/{id}", traceId)
49-
}
50+
server.assertNoEnvelope { isForRequest(it, id) }
5051
}
5152

5253
@Test
@@ -63,6 +64,8 @@ class DistributedTracingWebfluxIntegrationTest : WebfluxIntegrationTest() {
6364

6465
val incoming = awaitTransaction("GET /tracing/{id}", traceId).header.traceContext!!
6566
val outgoing = awaitTransaction("GET /person/{id}", traceId).header.traceContext!!
67+
assertThat(incoming.traceId).isEqualTo(traceId)
68+
assertThat(outgoing.traceId).isEqualTo(traceId)
6669
assertThat(incoming.sampleRand).isNotNull()
6770
assertThat(outgoing.sampleRand).isEqualTo(incoming.sampleRand)
6871
}
@@ -81,6 +84,8 @@ class DistributedTracingWebfluxIntegrationTest : WebfluxIntegrationTest() {
8184

8285
val incoming = awaitTransaction("GET /tracing/{id}", traceId).header.traceContext!!
8386
val outgoing = awaitTransaction("GET /person/{id}", traceId).header.traceContext!!
87+
assertThat(incoming.traceId).isEqualTo(traceId)
88+
assertThat(outgoing.traceId).isEqualTo(traceId)
8489
assertThat(incoming.sampleRate).isNotEqualTo("0.5")
8590
assertThat(outgoing.sampleRate).isEqualTo(incoming.sampleRate)
8691
}
@@ -130,6 +135,12 @@ class DistributedTracingWebfluxIntegrationTest : WebfluxIntegrationTest() {
130135
isTransaction(it, name, traceId)
131136
}
132137

138+
/** Whether [envelope] is a transaction for `/tracing/[id]` or the `/person/[id]` it calls. */
139+
private fun isForRequest(envelope: SentryEnvelope, id: Long): Boolean {
140+
val url = envelope.transaction()?.request?.url ?: return false
141+
return url.endsWith("/tracing/$id") || url.endsWith("/person/$id")
142+
}
143+
133144
private fun isTransaction(envelope: SentryEnvelope, name: String, traceId: SentryId): Boolean {
134145
val transaction = envelope.transaction() ?: return false
135146
return transaction.transaction == name && transaction.contexts.trace?.traceId == traceId
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.sentry.spring.boot.webfluxintegrationtest
2+
3+
import io.sentry.Sentry
4+
import io.sentry.metrics.MetricsUnit
5+
import org.springframework.web.bind.annotation.GetMapping
6+
import org.springframework.web.bind.annotation.PathVariable
7+
import org.springframework.web.bind.annotation.RequestMapping
8+
import org.springframework.web.bind.annotation.RestController
9+
10+
@RestController
11+
@RequestMapping("/metric/")
12+
open class MetricController {
13+
@GetMapping("count")
14+
open fun count(): String {
15+
Sentry.setAttribute("user.type", "admin")
16+
Sentry.setAttribute("feature.version", 2)
17+
Sentry.metrics().count("countMetric")
18+
return "count metric increased"
19+
}
20+
21+
@GetMapping("gauge/{value}")
22+
open fun gauge(@PathVariable("value") value: Long): String {
23+
Sentry.metrics().gauge("memory.free", value.toDouble(), MetricsUnit.Information.BYTE)
24+
return "gauge metric tracked"
25+
}
26+
27+
@GetMapping("distribution/{value}")
28+
open fun distribution(@PathVariable("value") value: Long): String {
29+
Sentry.metrics()
30+
.distribution("distributionMetric", value.toDouble(), MetricsUnit.Duration.MILLISECOND)
31+
return "distribution metric tracked"
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.sentry.spring.boot.webfluxintegrationtest
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import io.sentry.Sentry
5+
import io.sentry.SentryMetricsEvents
6+
import io.sentry.test.hasMetric
7+
import io.sentry.test.hasMetricWithAttribute
8+
import io.sentry.test.metrics
9+
import kotlin.test.Test
10+
11+
class MetricsWebfluxIntegrationTest : WebfluxIntegrationTest() {
12+
13+
@Test
14+
fun `count metric carries the scope's attributes`() {
15+
assertThat(restTemplate.getForObject("/metric/count", String::class.java))
16+
.isEqualTo("count metric increased")
17+
18+
val metrics = awaitMetric("countMetric")
19+
assertThat(metrics.hasMetric("countMetric", "counter", 1.0)).isTrue()
20+
assertThat(metrics.hasMetricWithAttribute("countMetric", "user.type", "admin")).isTrue()
21+
assertThat(metrics.hasMetricWithAttribute("countMetric", "feature.version", 2)).isTrue()
22+
}
23+
24+
@Test
25+
fun `gauge metric`() {
26+
assertThat(restTemplate.getForObject("/metric/gauge/14", String::class.java))
27+
.isEqualTo("gauge metric tracked")
28+
29+
assertThat(awaitMetric("memory.free").hasMetric("memory.free", "gauge", 14.0)).isTrue()
30+
}
31+
32+
@Test
33+
fun `distribution metric`() {
34+
assertThat(restTemplate.getForObject("/metric/distribution/23", String::class.java))
35+
.isEqualTo("distribution metric tracked")
36+
37+
assertThat(
38+
awaitMetric("distributionMetric").hasMetric("distributionMetric", "distribution", 23.0)
39+
)
40+
.isTrue()
41+
}
42+
43+
/** Flushes first because metrics are batched before they're sent. */
44+
private fun awaitMetric(name: String): SentryMetricsEvents {
45+
Sentry.flush(5000)
46+
return server
47+
.awaitEnvelope { it.metrics()?.items?.any { metric -> metric.name == name } == true }
48+
.metrics()!!
49+
}
50+
}

‎sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/WebfluxIntegrationTest.kt‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package io.sentry.spring.boot.webfluxintegrationtest
33
import io.sentry.test.MockSentryServer
44
import org.junit.Before
55
import org.junit.runner.RunWith
6-
import org.springframework.beans.factory.annotation.Autowired
6+
import org.springframework.beans.factory.annotation.Value
77
import org.springframework.boot.test.context.SpringBootTest
88
import org.springframework.boot.test.web.client.TestRestTemplate
9+
import org.springframework.boot.web.client.RestTemplateBuilder
910
import org.springframework.test.context.DynamicPropertyRegistry
1011
import org.springframework.test.context.DynamicPropertySource
1112
import org.springframework.test.context.junit4.SpringRunner
@@ -32,7 +33,13 @@ import org.springframework.test.context.junit4.SpringRunner
3233
],
3334
)
3435
abstract class WebfluxIntegrationTest {
35-
@Autowired lateinit var restTemplate: TestRestTemplate
36+
@Value("\${local.server.port}") private var port = 0
37+
38+
// The context's TestRestTemplate comes from the RestTemplateBuilder that Sentry customizes, so
39+
// it would add trace headers of its own to the requests tests send. Tests use a plain one.
40+
val restTemplate: TestRestTemplate by lazy {
41+
TestRestTemplate(RestTemplateBuilder().rootUri("http://localhost:$port"))
42+
}
3643

3744
@Before
3845
fun resetServer() {

0 commit comments

Comments
 (0)