From 78bc935535f226272369a7b88a3d3535d572f4fa Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 11 Nov 2026 16:16:52 +0100 Subject: [PATCH 1/2] feat(tracing): Filter configured incoming HTTP statuses Drop completed incoming server HTTP transactions when their root response status matches a configured range. Preserve lifecycle finalization while skipping processors, callbacks, envelope creation, and replay registration for filtered transactions. Record transaction, span, and profile losses, clean up temporary profile traces, and cover Spring Boot servlet and WebFlux integrations. Fixes #4735 --- CHANGELOG.md | 33 ++++ .../boot4/integrationtest/IntegrationTest.kt | 1 + .../integrationtest/PersonIntegrationTest.kt | 11 ++ .../PersonWebfluxIntegrationTest.kt | 11 ++ .../WebfluxIntegrationTest.kt | 1 + .../integrationtest/IntegrationTest.kt | 1 + .../integrationtest/PersonIntegrationTest.kt | 11 ++ .../PersonWebfluxIntegrationTest.kt | 11 ++ .../WebfluxIntegrationTest.kt | 1 + sentry-spring-boot/README.md | 13 ++ .../boot/integrationtest/IntegrationTest.kt | 1 + .../integrationtest/PersonIntegrationTest.kt | 11 ++ .../PersonWebfluxIntegrationTest.kt | 11 ++ .../WebfluxIntegrationTest.kt | 1 + .../src/main/java/io/sentry/SentryClient.java | 90 ++++++++++ .../test/java/io/sentry/SentryClientTest.kt | 165 +++++++++++++++++- .../test/java/io/sentry/SentryTracerTest.kt | 28 +++ 17 files changed, 400 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d0cec6fc1..4dce809da57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,39 @@ ### Features +- Drop incoming server transactions whose root HTTP response status matches `ignoredServerHttpStatusCodesForTracing`. Configure individual statuses such as `404`, inclusive ranges such as `400-499` or `500-599`, or combine them. + + Java: + + ```java + import io.sentry.HttpStatusCodeRange; + import java.util.Arrays; + + options.setIgnoredServerHttpStatusCodesForTracing( + Arrays.asList(new HttpStatusCodeRange(404), new HttpStatusCodeRange(500, 599))); + ``` + + `sentry.properties`: + + ```properties + ignored-server-http-status-codes-for-tracing=404,500-599 + ``` + + Spring Boot `application.properties`: + + ```properties + sentry.ignored-server-http-status-codes-for-tracing=404,500-599 + ``` + + Spring Boot `application.yml`: + + ```yaml + sentry: + ignored-server-http-status-codes-for-tracing: + - "404" + - "500-599" + ``` + - Monitor every Spring `@Scheduled` method with Sentry Crons by setting `sentry.monitor-scheduled-tasks=true`, without adding `@SentryCheckIn` to each method. Sentry creates each monitor from the method's `@Scheduled` schedule. Exclude methods with `sentry.exclude-scheduled-tasks` ([#6275](https://github.com/getsentry/sentry-java/pull/6275), [#6276](https://github.com/getsentry/sentry-java/pull/6276), [#6277](https://github.com/getsentry/sentry-java/pull/6277)) - Quartz `SentryJobListener` can send a monitor config derived from the trigger with check-ins, so Sentry creates or updates the monitor from code. Set the `sentry-upsert-monitor-config` job or trigger data key to `"true"` to send the schedule ([#6216](https://github.com/getsentry/sentry-java/pull/6216)) - Send a monitor config derived from Spring `@Scheduled` with `@SentryCheckIn` check-ins, so Sentry creates or updates the monitor from code. Set `@SentryCheckIn(upsertMonitorConfig = true)` to send the schedule ([#6215](https://github.com/getsentry/sentry-java/pull/6215)) diff --git a/sentry-spring-boot-4/src/integrationTest/kotlin/io/sentry/spring/boot4/integrationtest/IntegrationTest.kt b/sentry-spring-boot-4/src/integrationTest/kotlin/io/sentry/spring/boot4/integrationtest/IntegrationTest.kt index f61ad6c3826..3ca549b3b9e 100644 --- a/sentry-spring-boot-4/src/integrationTest/kotlin/io/sentry/spring/boot4/integrationtest/IntegrationTest.kt +++ b/sentry-spring-boot-4/src/integrationTest/kotlin/io/sentry/spring/boot4/integrationtest/IntegrationTest.kt @@ -29,6 +29,7 @@ import org.springframework.test.context.junit4.SpringRunner properties = [ "sentry.traces-sample-rate=1.0", + "sentry.ignored-server-http-status-codes-for-tracing=404", "sentry.clients.ok-http-enabled=true", "sentry.enable-cache-tracing=true", "sentry.enable-queue-tracing=true", 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 afb633e66e8..ee18a3b7233 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 @@ -102,6 +102,17 @@ class PersonIntegrationTest : IntegrationTest() { .isTrue() } + @Test + fun `configured not found response does not send a transaction`() { + val path = "/missing-ignored-status" + + val response = restTemplate.getForEntity(path, String::class.java) + + assertThat(response.statusCode.value()).isEqualTo(404) + Sentry.flush(5000) + server.assertNoEnvelope { it.transaction()?.request?.url?.endsWith(path) == true } + } + @Test fun `creating a person starts a profile linked to the transaction`() { val response = diff --git a/sentry-spring-boot-4/src/integrationTestWebflux/kotlin/io/sentry/spring/boot4/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt b/sentry-spring-boot-4/src/integrationTestWebflux/kotlin/io/sentry/spring/boot4/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt index 15aab17c410..7af3dbcfdf4 100644 --- a/sentry-spring-boot-4/src/integrationTestWebflux/kotlin/io/sentry/spring/boot4/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt +++ b/sentry-spring-boot-4/src/integrationTestWebflux/kotlin/io/sentry/spring/boot4/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt @@ -66,6 +66,17 @@ class PersonWebfluxIntegrationTest : WebfluxIntegrationTest() { .isEqualTo(200) } + @Test + fun `configured not found response does not send a transaction`() { + val path = "/missing-ignored-status" + + val response = restTemplate.getForEntity(path, String::class.java) + + assertThat(response.statusCode.value()).isEqualTo(404) + Sentry.flush(5000) + server.assertNoEnvelope { it.transaction()?.request?.url?.endsWith(path) == true } + } + @Test fun `creating a person starts a profile linked to the transaction`() { val response = diff --git a/sentry-spring-boot-4/src/integrationTestWebflux/kotlin/io/sentry/spring/boot4/webfluxintegrationtest/WebfluxIntegrationTest.kt b/sentry-spring-boot-4/src/integrationTestWebflux/kotlin/io/sentry/spring/boot4/webfluxintegrationtest/WebfluxIntegrationTest.kt index 360dcbb1617..2a9bb4d6710 100644 --- a/sentry-spring-boot-4/src/integrationTestWebflux/kotlin/io/sentry/spring/boot4/webfluxintegrationtest/WebfluxIntegrationTest.kt +++ b/sentry-spring-boot-4/src/integrationTestWebflux/kotlin/io/sentry/spring/boot4/webfluxintegrationtest/WebfluxIntegrationTest.kt @@ -26,6 +26,7 @@ import org.springframework.test.context.junit4.SpringRunner properties = [ "sentry.traces-sample-rate=1.0", + "sentry.ignored-server-http-status-codes-for-tracing=404", "sentry.enable-cache-tracing=true", "sentry.logs.enabled=true", "sentry.reactive.thread-local-accessor-enabled=true", diff --git a/sentry-spring-boot-jakarta/src/integrationTest/kotlin/io/sentry/spring/boot/jakarta/integrationtest/IntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTest/kotlin/io/sentry/spring/boot/jakarta/integrationtest/IntegrationTest.kt index d539470ae93..cb67a5321eb 100644 --- a/sentry-spring-boot-jakarta/src/integrationTest/kotlin/io/sentry/spring/boot/jakarta/integrationtest/IntegrationTest.kt +++ b/sentry-spring-boot-jakarta/src/integrationTest/kotlin/io/sentry/spring/boot/jakarta/integrationtest/IntegrationTest.kt @@ -29,6 +29,7 @@ import org.springframework.test.context.junit4.SpringRunner properties = [ "sentry.traces-sample-rate=1.0", + "sentry.ignored-server-http-status-codes-for-tracing=404", "sentry.clients.ok-http-enabled=true", "sentry.enable-cache-tracing=true", "sentry.enable-queue-tracing=true", 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 1728424fbc1..919034786d6 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 @@ -102,6 +102,17 @@ class PersonIntegrationTest : IntegrationTest() { .isTrue() } + @Test + fun `configured not found response does not send a transaction`() { + val path = "/missing-ignored-status" + + val response = restTemplate.getForEntity(path, String::class.java) + + assertThat(response.statusCode.value()).isEqualTo(404) + Sentry.flush(5000) + server.assertNoEnvelope { it.transaction()?.request?.url?.endsWith(path) == true } + } + @Test fun `creating a person starts a profile linked to the transaction`() { val response = diff --git a/sentry-spring-boot-jakarta/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/jakarta/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/jakarta/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt index 662ffc961dc..f437453e3b7 100644 --- a/sentry-spring-boot-jakarta/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/jakarta/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt +++ b/sentry-spring-boot-jakarta/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/jakarta/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt @@ -66,6 +66,17 @@ class PersonWebfluxIntegrationTest : WebfluxIntegrationTest() { .isEqualTo(200) } + @Test + fun `configured not found response does not send a transaction`() { + val path = "/missing-ignored-status" + + val response = restTemplate.getForEntity(path, String::class.java) + + assertThat(response.statusCode.value()).isEqualTo(404) + Sentry.flush(5000) + server.assertNoEnvelope { it.transaction()?.request?.url?.endsWith(path) == true } + } + @Test fun `creating a person starts a profile linked to the transaction`() { val response = diff --git a/sentry-spring-boot-jakarta/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/jakarta/webfluxintegrationtest/WebfluxIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/jakarta/webfluxintegrationtest/WebfluxIntegrationTest.kt index 4d314719e35..8f0ff2d0de3 100644 --- a/sentry-spring-boot-jakarta/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/jakarta/webfluxintegrationtest/WebfluxIntegrationTest.kt +++ b/sentry-spring-boot-jakarta/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/jakarta/webfluxintegrationtest/WebfluxIntegrationTest.kt @@ -26,6 +26,7 @@ import org.springframework.test.context.junit4.SpringRunner properties = [ "sentry.traces-sample-rate=1.0", + "sentry.ignored-server-http-status-codes-for-tracing=404", "sentry.enable-cache-tracing=true", "sentry.logs.enabled=true", "sentry.reactive.thread-local-accessor-enabled=true", diff --git a/sentry-spring-boot/README.md b/sentry-spring-boot/README.md index 45ac8d72649..beb1aa3d228 100644 --- a/sentry-spring-boot/README.md +++ b/sentry-spring-boot/README.md @@ -4,3 +4,16 @@ This integration is ideal to Spring Boot users. It buids on top of [Sentry's cor and also integrates well with logback or log4j2 if those are used in the application. For more information check out [Sentry Spring Boot documentation](https://docs.sentry.io/platforms/java/guides/spring-boot/). + +## Ignoring server transactions by HTTP status + +Set `sentry.ignored-server-http-status-codes-for-tracing` to a comma-separated list of individual +status codes and inclusive ranges: + +```properties +sentry.ignored-server-http-status-codes-for-tracing=404,500-599 +``` + +Matching incoming server transactions are not sent to Sentry. The filter reads the root +transaction's HTTP response status; child span statuses do not affect it. The option is disabled +when it is unset or empty. 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 a83f50a73f3..31e49fdbb64 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 @@ -29,6 +29,7 @@ import org.springframework.test.context.junit4.SpringRunner properties = [ "sentry.traces-sample-rate=1.0", + "sentry.ignored-server-http-status-codes-for-tracing=404", "sentry.clients.ok-http-enabled=true", "sentry.enable-cache-tracing=true", "sentry.enable-queue-tracing=true", diff --git a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/PersonIntegrationTest.kt b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/PersonIntegrationTest.kt index 045bfa37a96..fa17b781b01 100644 --- a/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/PersonIntegrationTest.kt +++ b/sentry-spring-boot/src/integrationTest/kotlin/io/sentry/spring/boot/integrationtest/PersonIntegrationTest.kt @@ -102,6 +102,17 @@ class PersonIntegrationTest : IntegrationTest() { .isTrue() } + @Test + fun `configured not found response does not send a transaction`() { + val path = "/missing-ignored-status" + + val response = restTemplate.getForEntity(path, String::class.java) + + assertThat(response.statusCode.value()).isEqualTo(404) + Sentry.flush(5000) + server.assertNoEnvelope { it.transaction()?.request?.url?.endsWith(path) == true } + } + @Test fun `creating a person starts a profile linked to the transaction`() { val response = diff --git a/sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt b/sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt index 0210da8ba64..35c00694d6f 100644 --- a/sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt +++ b/sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/PersonWebfluxIntegrationTest.kt @@ -66,6 +66,17 @@ class PersonWebfluxIntegrationTest : WebfluxIntegrationTest() { .isEqualTo(200) } + @Test + fun `configured not found response does not send a transaction`() { + val path = "/missing-ignored-status" + + val response = restTemplate.getForEntity(path, String::class.java) + + assertThat(response.statusCode.value()).isEqualTo(404) + Sentry.flush(5000) + server.assertNoEnvelope { it.transaction()?.request?.url?.endsWith(path) == true } + } + @Test fun `creating a person starts a profile linked to the transaction`() { val response = diff --git a/sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/WebfluxIntegrationTest.kt b/sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/WebfluxIntegrationTest.kt index f222e37b4ef..b2ee1979640 100644 --- a/sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/WebfluxIntegrationTest.kt +++ b/sentry-spring-boot/src/integrationTestWebflux/kotlin/io/sentry/spring/boot/webfluxintegrationtest/WebfluxIntegrationTest.kt @@ -26,6 +26,7 @@ import org.springframework.test.context.junit4.SpringRunner properties = [ "sentry.traces-sample-rate=1.0", + "sentry.ignored-server-http-status-codes-for-tracing=404", "sentry.logs.enabled=true", "sentry.profile-session-sample-rate=1.0", "sentry.profile-lifecycle=TRACE", diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index e0b1901f9b8..22673f13121 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -21,6 +21,7 @@ import io.sentry.transport.RateLimiter; import io.sentry.util.*; import java.io.Closeable; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -1024,6 +1025,30 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return SentryId.EMPTY_ID; } + final @Nullable Integer ignoredHttpStatusCode = getIgnoredHttpStatusCode(transaction); + if (ignoredHttpStatusCode != null) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Transaction was dropped as HTTP status code %d is ignored", + ignoredHttpStatusCode); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.Transaction); + options + .getClientReportRecorder() + .recordLostEvent( + DiscardReason.EVENT_PROCESSOR, DataCategory.Span, transaction.getSpans().size() + 1); + if (profilingTraceData != null) { + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.Profile); + } + deleteProfilingTraceFile(profilingTraceData); + return SentryId.EMPTY_ID; + } + SentryId sentryId = SentryId.EMPTY_ID; if (transaction.getEventId() != null) { sentryId = transaction.getEventId(); @@ -1096,6 +1121,71 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return sentryId; } + private @Nullable Integer getIgnoredHttpStatusCode(final @NotNull SentryTransaction transaction) { + final @Nullable List ignoredHttpStatusCodes = + options.getIgnoredServerHttpStatusCodesForTracing(); + if (ignoredHttpStatusCodes == null || ignoredHttpStatusCodes.isEmpty()) { + return null; + } + + final @Nullable SpanContext trace = transaction.getContexts().getTrace(); + if (trace == null + || !"server".equals(trace.getData().get(SpanDataConvention.SENTRY_KIND_KEY))) { + return null; + } + + final @Nullable Object httpStatusCodeValue = + trace.getData().get(SpanDataConvention.HTTP_STATUS_CODE_KEY); + final int httpStatusCode; + if (httpStatusCodeValue instanceof Integer) { + httpStatusCode = (Integer) httpStatusCodeValue; + } else if (httpStatusCodeValue instanceof Long) { + final long longHttpStatusCode = (Long) httpStatusCodeValue; + if (longHttpStatusCode < 100 || longHttpStatusCode > 599) { + return null; + } + httpStatusCode = (int) longHttpStatusCode; + } else { + return null; + } + + if (httpStatusCode < 100 || httpStatusCode > 599) { + return null; + } + for (final @Nullable HttpStatusCodeRange ignoredHttpStatusCode : ignoredHttpStatusCodes) { + if (ignoredHttpStatusCode != null && ignoredHttpStatusCode.isInRange(httpStatusCode)) { + return httpStatusCode; + } + } + return null; + } + + private void deleteProfilingTraceFile(final @Nullable ProfilingTraceData profilingTraceData) { + if (profilingTraceData == null) { + return; + } + + final @NotNull File traceFile = profilingTraceData.getTraceFile(); + try { + if (!traceFile.delete()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Failed to delete profiling trace file %s for an ignored transaction", + traceFile.getPath()); + } + } catch (SecurityException e) { + options + .getLogger() + .log( + SentryLevel.WARNING, + e, + "Failed to delete profiling trace file %s for an ignored transaction", + traceFile.getPath()); + } + } + @ApiStatus.Internal @Override public @NotNull SentryId captureProfileChunk( diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 6e359e56b9f..e11920d5e0d 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -89,6 +89,7 @@ class SentryClientTest { var loggerBatchProcessorFactory = mock() var metricsBatchProcessor = mock() var metricsBatchProcessorFactory = mock() + val logger = mock() val maxAttachmentSize: Long = (5 * 1024 * 1024).toLong() val scopes = mock() val sentryTracer: SentryTracer @@ -102,7 +103,7 @@ class SentryClientTest { isDebug = true setDiagnosticLevel(SentryLevel.DEBUG) setSerializer(JsonSerializer(this)) - setLogger(mock()) + setLogger(this@Fixture.logger) maxAttachmentSize = this@Fixture.maxAttachmentSize setTransportFactory(factory) logs.setLoggerBatchProcessorFactory(loggerBatchProcessorFactory) @@ -1383,6 +1384,161 @@ class SentryClientTest { assertEquals(SentryId.EMPTY_ID, eventId) } + @Test + fun `server transaction with ignored HTTP status is dropped before callbacks`() { + fixture.sentryOptions.setIgnoredServerHttpStatusCodesForTracing( + listOf(HttpStatusCodeRange(404)) + ) + val processor = mock() + val beforeSend = mock() + val replayController = mock() + fixture.sentryOptions.addEventProcessor(processor) + fixture.sentryOptions.beforeSendTransaction = beforeSend + fixture.sentryOptions.setReplayController(replayController) + val transaction = createServerTransaction(404) + + val eventId = + fixture + .getSut() + .captureTransaction( + transaction, + fixture.sentryTracer.traceContext(), + null, + null, + fixture.profilingTraceData, + ) + + assertThat(eventId).isEqualTo(SentryId.EMPTY_ID) + assertThat(fixture.profilingTraceFile.exists()).isFalse() + verify(processor, never()).process(any(), anyOrNull()) + verify(beforeSend, never()).execute(any(), anyOrNull()) + verifyNoInteractions(replayController) + verify(fixture.transport, never()).send(any(), anyOrNull()) + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf( + DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Transaction.category, 1), + DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2), + DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Profile.category, 1), + ), + ) + } + + @Test + fun `server transaction with ignored HTTP status range accepts OTel long value`() { + fixture.sentryOptions.setIgnoredServerHttpStatusCodesForTracing( + listOf(HttpStatusCodeRange(500, 599)) + ) + + val eventId = + fixture + .getSut() + .captureTransaction(createServerTransaction(503L), fixture.sentryTracer.traceContext()) + + assertThat(eventId).isEqualTo(SentryId.EMPTY_ID) + verify(fixture.transport, never()).send(any(), anyOrNull()) + } + + @Test + fun `HTTP status filter is disabled for an empty configuration`() { + fixture.sentryOptions.setIgnoredServerHttpStatusCodesForTracing(emptyList()) + + fixture + .getSut() + .captureTransaction(createServerTransaction(404), fixture.sentryTracer.traceContext()) + + verify(fixture.transport).send(any(), anyOrNull()) + } + + @Test + fun `HTTP status filter only applies to server transactions`() { + fixture.sentryOptions.setIgnoredServerHttpStatusCodesForTracing( + listOf(HttpStatusCodeRange(404)) + ) + val clientTransaction = createServerTransaction(404) + clientTransaction.contexts.trace!!.data[SpanDataConvention.SENTRY_KIND_KEY] = "client" + val missingKindTransaction = createServerTransaction(404) + missingKindTransaction.contexts.trace!!.data.remove(SpanDataConvention.SENTRY_KIND_KEY) + val sut = fixture.getSut() + + sut.captureTransaction(clientTransaction, fixture.sentryTracer.traceContext()) + sut.captureTransaction(missingKindTransaction, fixture.sentryTracer.traceContext()) + + verify(fixture.transport, times(2)).send(any(), anyOrNull()) + } + + @Test + fun `HTTP status filter ignores missing and invalid root status values`() { + fixture.sentryOptions.setIgnoredServerHttpStatusCodesForTracing( + listOf(HttpStatusCodeRange(404)) + ) + val invalidStatusValues = listOf("404", 404.0, 404.5f, 99, 600, Long.MAX_VALUE) + val sut = fixture.getSut() + + invalidStatusValues.forEach { statusCode -> + sut.captureTransaction( + createServerTransaction(statusCode), + fixture.sentryTracer.traceContext(), + ) + } + val missingStatusTransaction = createServerTransaction(200) + missingStatusTransaction.contexts.trace!!.data.remove(SpanDataConvention.HTTP_STATUS_CODE_KEY) + sut.captureTransaction(missingStatusTransaction, fixture.sentryTracer.traceContext()) + + verify(fixture.transport, times(invalidStatusValues.size + 1)).send(any(), anyOrNull()) + } + + @Test + fun `HTTP status filter does not read child span attributes`() { + fixture.sentryOptions.setIgnoredServerHttpStatusCodesForTracing( + listOf(HttpStatusCodeRange(404)) + ) + val tracer = + SentryTracer( + TransactionContext("child-status-transaction", "op", TracesSamplingDecision(true)), + fixture.scopes, + ) + tracer.startChild("child").apply { + setData(SpanDataConvention.SENTRY_KIND_KEY, "server") + setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, 404) + finish() + } + val transaction = SentryTransaction(tracer) + + fixture.getSut().captureTransaction(transaction, tracer.traceContext()) + + verify(fixture.transport).send(any(), anyOrNull()) + } + + @Test + fun `profiling trace deletion failure for ignored transaction is logged`() { + fixture.sentryOptions.setIgnoredServerHttpStatusCodesForTracing( + listOf(HttpStatusCodeRange(404)) + ) + val traceFile = mock() + whenever(traceFile.delete()).thenReturn(false) + whenever(traceFile.path).thenReturn("undeletable.trace") + val profilingTraceData = ProfilingTraceData(traceFile, fixture.sentryTracer) + + fixture + .getSut() + .captureTransaction( + createServerTransaction(404), + fixture.sentryTracer.traceContext(), + null, + null, + profilingTraceData, + ) + + verify(traceFile).delete() + verify(fixture.logger) + .log( + SentryLevel.WARNING, + "Failed to delete profiling trace file %s for an ignored transaction", + "undeletable.trace", + ) + } + @Test fun `backfillable events are only wired through backfilling processors`() { val backfillingProcessor = mock() @@ -4536,6 +4692,13 @@ class SentryClientTest { } } + private fun createServerTransaction(statusCode: Any): SentryTransaction { + return SentryTransaction(fixture.sentryTracer).apply { + contexts.trace!!.data[SpanDataConvention.SENTRY_KIND_KEY] = "server" + contexts.trace!!.data[SpanDataConvention.HTTP_STATUS_CODE_KEY] = statusCode + } + } + private fun createSession(release: String = "rel"): Session { return Session("dis", User(), "env", release) } diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 20232f56b4d..0643e760365 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -239,6 +239,34 @@ class SentryTracerTest { verify(transactionProfiler).onTransactionFinish(any(), anyOrNull(), anyOrNull()) } + @Test + fun `ignored HTTP status configuration does not skip profiling and performance finalization`() { + val transactionProfiler = mock() + val performanceCollector = mock() + val profilingTraceData = mock() + whenever(transactionProfiler.onTransactionFinish(any(), anyOrNull(), anyOrNull())) + .thenReturn(profilingTraceData) + val tracer = + fixture.getSut( + optionsConfiguration = { + it.profilesSampleRate = 1.0 + it.setTransactionProfiler(transactionProfiler) + it.setIgnoredServerHttpStatusCodesForTracing(listOf(HttpStatusCodeRange(404))) + }, + samplingDecision = TracesSamplingDecision(true, null, true, null), + performanceCollector = performanceCollector, + ) + tracer.setData(SpanDataConvention.SENTRY_KIND_KEY, "server") + tracer.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, 404) + + tracer.finish() + + verify(performanceCollector).stop(tracer) + verify(transactionProfiler).onTransactionFinish(eq(tracer), anyOrNull(), eq(fixture.options)) + verify(fixture.scopes) + .captureTransaction(any(), anyOrNull(), anyOrNull(), eq(profilingTraceData)) + } + @Test fun `when continuous profiler is running, profile context is set`() { val continuousProfiler = mock() From c88d883f7cfae40ba4740aad1589937c37508638 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 11 Nov 2026 16:17:43 +0100 Subject: [PATCH 2/2] chore: Link HTTP status filter changelog entry Reference PR #6566 from the user-facing feature entry now that the stacked pull request has been created. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dce809da57..fc1f5ccece3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ ### Features -- Drop incoming server transactions whose root HTTP response status matches `ignoredServerHttpStatusCodesForTracing`. Configure individual statuses such as `404`, inclusive ranges such as `400-499` or `500-599`, or combine them. +- Drop incoming server transactions whose root HTTP response status matches `ignoredServerHttpStatusCodesForTracing`. Configure individual statuses such as `404`, inclusive ranges such as `400-499` or `500-599`, or combine them ([#6566](https://github.com/getsentry/sentry-java/pull/6566)). Java: