From 2c84527d946bb56b1f462959c8de26ad22a1f305 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 24 Sep 2026 16:16:08 +0200 Subject: [PATCH 1/2] fix(core): [Callback Errors 6] Handle profilesSampler failures Disable profiling when profilesSampler throws instead of applying the static profilesSampleRate or inheriting a parent profiling decision. Preserve trace sampling metadata and normal null-result fallback. Cover static-rate bypass, parent inheritance, and failures in both samplers without changing existing catch types. Refs #6081 Co-Authored-By: Claude --- CHANGELOG.md | 1 + .../main/java/io/sentry/TracesSampler.java | 11 ++- .../test/java/io/sentry/TracesSamplerTest.kt | 93 +++++++++++++++++-- 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f7b506447..5df28f856c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,7 @@ - Drop telemetry and record `callback_error` when a customer event processor throws instead of continuing with a potentially partially processed item. SDK-owned processor failures are logged and processing continues without a `callback_error` client report. - Drop breadcrumbs when `beforeBreadcrumb` throws instead of storing exception details on the breadcrumb. - When `tracesSampler` throws, inherit the parent sampling decision or leave the trace unsampled if there is no parent decision, instead of falling back to `tracesSampleRate` ([#6163](https://github.com/getsentry/sentry-java/pull/6163)) + - When `profilesSampler` throws, disable profiling instead of falling back to `profilesSampleRate` or inheriting the parent's profiling decision. Trace sampling is unchanged. - Disable URL caching when reading `META-INF/MANIFEST.MF` files during version detection so that the SDK no longer keeps jar file handles open for the life of the process ([#6124](https://github.com/getsentry/sentry-java/pull/6124) - Keep the `EventListener` wrapped by `SentryOkHttpEventListener` per `Call` ([#6003](https://github.com/getsentry/sentry-java/pull/6003)) diff --git a/sentry/src/main/java/io/sentry/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index e981bb6f66f..cb73310e694 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -25,16 +25,18 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont } Double profilesSampleRate = null; + boolean profilesSamplerFailed = false; if (options.getProfilesSampler() != null) { try { profilesSampleRate = options.getProfilesSampler().sample(samplingContext); } catch (Throwable t) { + profilesSamplerFailed = true; options .getLogger() .log(SentryLevel.ERROR, "Error in the 'ProfilesSamplerCallback' callback.", t); } } - if (profilesSampleRate == null) { + if (profilesSampleRate == null && !profilesSamplerFailed) { profilesSampleRate = options.getProfilesSampleRate(); } Boolean profilesSampled = profilesSampleRate != null && sample(profilesSampleRate, sampleRand); @@ -63,6 +65,13 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont final TracesSamplingDecision parentSamplingDecision = samplingContext.getTransactionContext().getParentSamplingDecision(); if (parentSamplingDecision != null) { + if (profilesSamplerFailed) { + return SampleRateUtils.backfilledSampleRand( + new TracesSamplingDecision( + parentSamplingDecision.getSampled(), + parentSamplingDecision.getSampleRate(), + parentSamplingDecision.getSampleRand())); + } return SampleRateUtils.backfilledSampleRand(parentSamplingDecision); } diff --git a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt index bb257a9c46e..e5b97b34e7c 100644 --- a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt +++ b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt @@ -191,7 +191,7 @@ class TracesSamplerTest { @Test fun `when profilesSampler returns null and parentSampled is set sampler uses it as a sampling decision`() { - val sampler = fixture.getSut(tracesSampleRate = 1.0, profilesSamplerCallback = null) + val sampler = fixture.getSut(tracesSampleRate = 1.0, profilesSamplerCallback = { null }) val transactionContextParentSampled = TransactionContext("name", "op") transactionContextParentSampled.setParentSampled(true, true) val samplingDecision = @@ -222,7 +222,7 @@ class TracesSamplerTest { fixture.getSut( tracesSampleRate = 1.0, profilesSampleRate = 0.2, - profilesSamplerCallback = null, + profilesSamplerCallback = { null }, ) val samplingDecision = sampler.sample( @@ -355,18 +355,93 @@ class TracesSamplerTest { } @Test - fun `when a profilingRate and a ProfilesSamplerCallback is set but the callback throws an exception then profiling should still be enabled`() { - val exception = Exception("faulty ProfilesSamplerCallback") + fun `when profilesSampler throws then static profile rates are ignored`() { + for (profilesSampleRate in listOf(null, 0.0, 1.0)) { + val sampler = + fixture.getSut( + tracesSampleRate = 1.0, + profilesSampleRate = profilesSampleRate, + profilesSamplerCallback = { + throw IllegalStateException("faulty ProfilesSamplerCallback") + }, + ) + val decision = + sampler.sample(SamplingContext(TransactionContext("name", "op"), null, 0.0, null)) + + assertThat(decision.sampled).isTrue() + assertThat(decision.sampleRate).isEqualTo(1.0) + assertThat(decision.sampleRand).isEqualTo(0.0) + assertThat(decision.profileSampled).isFalse() + assertThat(decision.profileSampleRate).isNull() + } + } + + @Test + fun `when profilesSampler throws then tracesSampler still determines trace sampling`() { val sampler = fixture.getSut( - tracesSampleRate = 1.0, + tracesSampleRate = 0.0, profilesSampleRate = 1.0, - profilesSamplerCallback = { throw exception }, + tracesSamplerCallback = { 0.5 }, + profilesSamplerCallback = { throw IllegalStateException("faulty ProfilesSamplerCallback") }, ) val decision = - sampler.sample(SamplingContext(TransactionContext("name", "op"), null, 0.0, null)) - assertTrue(decision.profileSampled) - assertEquals(0.0, decision.sampleRand) + sampler.sample(SamplingContext(TransactionContext("name", "op"), null, 0.1, null)) + + assertThat(decision.sampled).isTrue() + assertThat(decision.sampleRate).isEqualTo(0.5) + assertThat(decision.sampleRand).isEqualTo(0.1) + assertThat(decision.profileSampled).isFalse() + assertThat(decision.profileSampleRate).isNull() + } + + @Test + fun `when profilesSampler throws then parent trace sampling is preserved without profiling`() { + val sampler = + fixture.getSut( + tracesSampleRate = 1.0, + profilesSampleRate = 1.0, + profilesSamplerCallback = { throw IllegalStateException("faulty ProfilesSamplerCallback") }, + ) + for (sampled in listOf(true, false)) { + val sampleRand = if (sampled) 0.1 else 0.9 + val parentDecision = TracesSamplingDecision(sampled, 0.5, sampleRand, true, 1.0) + val transactionContext = + TransactionContext(SentryId(), SpanId(), SpanId(), parentDecision, null) + + val decision = sampler.sample(SamplingContext(transactionContext, null, sampleRand, null)) + + assertThat(decision.sampled).isEqualTo(sampled) + assertThat(decision.sampleRate).isEqualTo(0.5) + assertThat(decision.sampleRand).isEqualTo(sampleRand) + assertThat(decision.profileSampled).isFalse() + assertThat(decision.profileSampleRate).isNull() + assertThat(parentDecision.profileSampled).isEqualTo(sampled) + assertThat(parentDecision.profileSampleRate).isEqualTo(1.0) + } + } + + @Test + fun `when both samplers throw then parent trace sampling is backfilled without profiling`() { + val sampler = + fixture.getSut( + tracesSampleRate = 0.0, + profilesSampleRate = 1.0, + tracesSamplerCallback = { throw IllegalStateException("faulty TracesSamplerCallback") }, + profilesSamplerCallback = { throw IllegalStateException("faulty ProfilesSamplerCallback") }, + ) + val parentDecision = TracesSamplingDecision(true, 0.5, true, 1.0) + val transactionContext = + TransactionContext(SentryId(), SpanId(), SpanId(), parentDecision, null) + + val decision = sampler.sample(SamplingContext(transactionContext, null, 0.9, null)) + + assertThat(decision.sampled).isTrue() + assertThat(decision.sampleRate).isEqualTo(0.5) + assertThat(decision.sampleRand).isAtLeast(0.0) + assertThat(decision.sampleRand).isLessThan(0.5) + assertThat(decision.profileSampled).isFalse() + assertThat(decision.profileSampleRate).isNull() } @Test From aa1516454d40b256dec624b256bc14ac33a2d745 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 24 Sep 2026 16:35:20 +0200 Subject: [PATCH 2/2] changelog Link the profilesSampler failure handling entry to #6164 in the callback error handling stack. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df28f856c2..81f3c86b49a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,7 +127,7 @@ - Drop telemetry and record `callback_error` when a customer event processor throws instead of continuing with a potentially partially processed item. SDK-owned processor failures are logged and processing continues without a `callback_error` client report. - Drop breadcrumbs when `beforeBreadcrumb` throws instead of storing exception details on the breadcrumb. - When `tracesSampler` throws, inherit the parent sampling decision or leave the trace unsampled if there is no parent decision, instead of falling back to `tracesSampleRate` ([#6163](https://github.com/getsentry/sentry-java/pull/6163)) - - When `profilesSampler` throws, disable profiling instead of falling back to `profilesSampleRate` or inheriting the parent's profiling decision. Trace sampling is unchanged. + - When `profilesSampler` throws, disable profiling instead of falling back to `profilesSampleRate` or inheriting the parent's profiling decision. Trace sampling is unchanged ([#6164](https://github.com/getsentry/sentry-java/pull/6164)) - Disable URL caching when reading `META-INF/MANIFEST.MF` files during version detection so that the SDK no longer keeps jar file handles open for the life of the process ([#6124](https://github.com/getsentry/sentry-java/pull/6124) - Keep the `EventListener` wrapped by `SentryOkHttpEventListener` per `Call` ([#6003](https://github.com/getsentry/sentry-java/pull/6003))