Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix incoming defer sampling decision sentry-trace header #3942

Merged
merged 11 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Unreleased

### Fixes

- Fix incoming defer sampling decision `sentry-trace` header ([#3942](https://github.com/getsentry/sentry-java/pull/3942))
- A `sentry-trace` header that only contains trace ID and span ID but no sampled flag (`-1`, `-0` suffix) means the receiving system can make its own sampling decision
- When generating `sentry-trace` header from `PropagationContext` we now copy the `sampled` flag.
- In `TransactionContext.fromPropagationContext` when there is no parent sampling decision, keep the decision `null` so a new sampling decision is made instead of defaulting to `false`

## 8.0.0-rc.1

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,48 @@ class PersonSystemTest {
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
}
}

@Test
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO use @BeforeTest instead of @Before as it looks like it's not resetting between tests in the same test class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO copy this test over to other samples (e.g. jakarta version of this)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do this in a follow up PR

fun `create person creates transaction if no sampled flag in sentry-trace header`() {
adinauer marked this conversation as resolved.
Show resolved Hide resolved
val restClient = testHelper.restClient
val person = Person("firstA", "lastB")
val returnedPerson = restClient.createPerson(
person,
mapOf(
"sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee",
"baggage" to "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-transaction=HTTP%20GET"
)
)
assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode)

assertEquals(person.firstName, returnedPerson!!.firstName)
assertEquals(person.lastName, returnedPerson!!.lastName)

testHelper.ensureTransactionReceived { transaction ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
}
}

@Test
fun `create person creates transaction if sampled true in sentry-trace header`() {
val restClient = testHelper.restClient
val person = Person("firstA", "lastB")
val returnedPerson = restClient.createPerson(
person,
mapOf(
"sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1",
"baggage" to "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-transaction=HTTP%20GET"
)
)
assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode)

assertEquals(person.firstName, returnedPerson!!.firstName)
assertEquals(person.lastName, returnedPerson!!.lastName)

testHelper.ensureTransactionReceived { transaction ->
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl
}
}

fun createPerson(person: Person): Person? {
fun createPerson(person: Person, extraHeaders: Map<String, String>? = null): Person? {
return try {
val response = restTemplate().exchange("$backendBaseUrl/person/", HttpMethod.POST, entityWithAuth(person), Person::class.java, person)
val response = restTemplate().exchange("$backendBaseUrl/person/", HttpMethod.POST, entityWithAuth(person, extraHeaders), Person::class.java, person)
lastKnownStatusCode = response.statusCode
response.body
} catch (e: HttpStatusCodeException) {
Expand Down Expand Up @@ -55,9 +55,12 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl
}
}

private fun entityWithAuth(request: Any? = null): HttpEntity<Any?> {
val headers = HttpHeaders().also {
it.setBasicAuth("user", "password")
private fun entityWithAuth(request: Any? = null, extraHeaders: Map<String, String>? = null): HttpEntity<Any?> {
val headers = HttpHeaders().also { httpHeaders ->
httpHeaders.setBasicAuth("user", "password")
extraHeaders?.forEach { key, value ->
httpHeaders.set(key, value)
}
}

return HttpEntity<Any?>(request, headers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
import org.springframework.web.client.RestClient
import org.springframework.web.client.toEntity
import java.time.Duration
import java.util.concurrent.TimeUnit
import kotlin.test.Test
Expand Down Expand Up @@ -195,10 +196,11 @@ class SentrySpanRestClientCustomizerTest {
.get()
.uri(fixture.url)
.retrieve()
.toEntity(String::class.java)
adinauer marked this conversation as resolved.
Show resolved Hide resolved
} catch (t: Throwable) {
println(t)
}
// fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!!
fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!!
adinauer marked this conversation as resolved.
Show resolved Hide resolved
assertThat(fixture.transaction.spans).hasSize(1)
val span = fixture.transaction.spans.first()
assertThat(span.operation).isEqualTo("http.client")
Expand Down
11 changes: 6 additions & 5 deletions sentry/src/main/java/io/sentry/TransactionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ public static TransactionContext fromPropagationContext(
baggage.freeze();

Double sampleRate = baggage.getSampleRateDouble();
Boolean sampled = parentSampled != null ? parentSampled.booleanValue() : false;
if (sampleRate != null) {
samplingDecision = new TracesSamplingDecision(sampled, sampleRate);
} else {
samplingDecision = new TracesSamplingDecision(sampled);
if (parentSampled != null) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now uses null sampling decision instead of false for incoming sentry-trace header with deferred sampling decision. This causes sentry sampler to make a decision instead of assuming false from parent should be copied.

if (sampleRate != null) {
samplingDecision = new TracesSamplingDecision(parentSampled.booleanValue(), sampleRate);
} else {
samplingDecision = new TracesSamplingDecision(parentSampled.booleanValue());
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion sentry/src/main/java/io/sentry/util/TracingUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ public static void startNewTrace(final @NotNull IScopes scopes) {

return new TracingHeaders(
new SentryTraceHeader(
propagationContext.getTraceId(), propagationContext.getSpanId(), null),
propagationContext.getTraceId(),
propagationContext.getSpanId(),
propagationContext.isSampled()),
adinauer marked this conversation as resolved.
Show resolved Hide resolved
baggageHeader);
}

Expand Down
17 changes: 17 additions & 0 deletions sentry/src/test/java/io/sentry/ScopesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1968,6 +1968,23 @@ class ScopesTest {
assertEquals(parentSpanId, transactionContext!!.parentSpanId)
}

@Test
fun `continueTrace creates propagation context from headers and returns transaction context if performance enabled no sampled value`() {
val scopes = generateScopes()
val traceId = SentryId()
val parentSpanId = SpanId()
val transactionContext = scopes.continueTrace("$traceId-$parentSpanId", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET"))

scopes.configureScope { scope ->
assertEquals(traceId, scope.propagationContext.traceId)
assertEquals(parentSpanId, scope.propagationContext.parentSpanId)
}

assertEquals(traceId, transactionContext!!.traceId)
assertEquals(parentSpanId, transactionContext!!.parentSpanId)
assertEquals(null, transactionContext!!.parentSamplingDecision)
}

@Test
fun `continueTrace creates new propagation context if header invalid and returns transaction context if performance enabled`() {
val scopes = generateScopes()
Expand Down
50 changes: 50 additions & 0 deletions sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class TracingUtilsTest {
assertNotNull(headers.baggageHeader)
assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId)
assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId)
assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled)
assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value"))
assertTrue(headers.baggageHeader!!.value.contains("sentry-trace_id=${fixture.scope.propagationContext.traceId}"))
assertFalse(fixture.scope.propagationContext.baggage!!.isMutable)
Expand All @@ -73,6 +74,55 @@ class TracingUtilsTest {
assertNotNull(headers.baggageHeader)
assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId)
assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId)
assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled)
assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value"))
assertFalse(fixture.scope.propagationContext.baggage!!.isMutable)
}

@Test
fun `returns headers if allowed from scope if span is noop sampled=null`() {
fixture.setup()
fixture.scope.propagationContext.isSampled = null

val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, NoOpSpan.getInstance())

assertNotNull(headers)
assertNotNull(headers.baggageHeader)
assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId)
assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId)
assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled)
assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value"))
assertFalse(fixture.scope.propagationContext.baggage!!.isMutable)
}

@Test
fun `returns headers if allowed from scope if span is noop sampled=true`() {
fixture.setup()
fixture.scope.propagationContext.isSampled = true

val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, NoOpSpan.getInstance())

assertNotNull(headers)
assertNotNull(headers.baggageHeader)
assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId)
assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId)
assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled)
assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value"))
assertFalse(fixture.scope.propagationContext.baggage!!.isMutable)
}

@Test
fun `returns headers if allowed from scope if span is noop sampled=false`() {
fixture.setup()
fixture.scope.propagationContext.isSampled = false

val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, NoOpSpan.getInstance())

assertNotNull(headers)
assertNotNull(headers.baggageHeader)
assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId)
assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId)
assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled)
assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value"))
assertFalse(fixture.scope.propagationContext.baggage!!.isMutable)
}
Expand Down
Loading