Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c354d31
test: normalize ContinueTrace tests
Flash0ver Jul 21, 2025
37908a3
test: use Assertion over type-cast
Flash0ver Jul 21, 2025
23a45c2
fix: DSC contains actual SampleRate
Flash0ver Jul 21, 2025
5cace1f
test: fix catch and handle failed Assertions
Flash0ver Jul 21, 2025
4401a98
style: reformat HubTests
Flash0ver Jul 21, 2025
3197ab6
test: fix HubTests
Flash0ver Jul 21, 2025
bc91dd3
docs: update CHANGELOG.md
Flash0ver Jul 21, 2025
755bbef
test: skip failing test on Android & iOS (device tests)
Flash0ver Jul 23, 2025
6259233
fix: DSC sample_rate when sampling forced
Flash0ver Jul 24, 2025
fd39406
test: fix PushAndLockScope when GlobalMode
Flash0ver Jul 24, 2025
3be7eee
Merge branch 'main' into fix/update-dsc-sample-rate
Flash0ver Jul 24, 2025
cd5355b
fix: forced sampling is only considered when TracesSampler has not de…
Flash0ver Jul 24, 2025
8dbe5e3
fix: no longer overwrite DSC sample_rate when upstream sampling decis…
Flash0ver Jul 25, 2025
2dc8666
Merge branch 'main' into fix/update-dsc-sample-rate
Flash0ver Jul 25, 2025
fa43554
fix: overwrite DSC sample_rate only when TracesSampler or TracesSampl…
Flash0ver Jul 28, 2025
2382943
ref: remove Debug.Assert
Flash0ver Jul 28, 2025
8c5588f
docs: add comment to test
Flash0ver Jul 28, 2025
50edc11
Merge branch 'main' into fix/update-dsc-sample-rate
Flash0ver Jul 30, 2025
758028c
docs: update CHANGELOG
Flash0ver Jul 30, 2025
a13b7e0
fix: TransactionTracer was assumed to be considered even when TracesS…
Flash0ver Jul 30, 2025
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
Prev Previous commit
Next Next commit
fix: overwrite DSC sample_rate only when TracesSampler or TracesSampl…
…eRate reached a sampling decision
  • Loading branch information
Flash0ver committed Jul 28, 2025
commit fa43554acb70f8d7886ed5b974879a17ec80213b
20 changes: 15 additions & 5 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,17 +188,27 @@ internal ITransactionTracer StartTransaction(
{
// The TracesSampler trumps all other sampling decisions (even the trace header)
sampleRate = samplerSampleRate;
isSampled = SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);
isSampled = SampleRandHelper.IsSampled(sampleRand, samplerSampleRate);

// Ensure the actual sampleRate is set on the provided DSC (if any) when the TracesSampler reached a sampling decision
dynamicSamplingContext = dynamicSamplingContext?.WithSampleRate(samplerSampleRate);
}
}

// If the sampling decision isn't made by a trace sampler we check the trace header first (from the context) or
// finally fallback to Random sampling if the decision has been made by no other means
sampleRate ??= _options.TracesSampleRate ?? 0.0;
isSampled ??= context.IsSampled ?? SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);
if (sampleRate == null)
{
Debug.Assert(isSampled == null);
sampleRate = _options.TracesSampleRate ?? 0.0;
isSampled = context.IsSampled ?? SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);

// Ensure the actual sampleRate is set on the provided DSC (if any)
dynamicSamplingContext = dynamicSamplingContext?.WithSampleRate(sampleRate.Value);
if (context.IsSampled is null && _options.TracesSampleRate is not null)
Copy link
Collaborator

@jamescrosswell jamescrosswell Jul 29, 2025

Choose a reason for hiding this comment

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

TLDR; I think we can remove the if block here.

Long version

Apologies, I think I got us confused on Slack:

  • context is of type ITransactionContext... I think when an upstream span is sampled, context.IsParentSampled will be true (not context.IsSampled).
  • Whether or not the parent span was sampled is probably not relevant. It's possible that our process has a different sample-rate configured to the upstream node... as long as we use the same sample rand, we maximise (not guarantee) the chance of having a complete trace. If we have a sample-rand of 0.5 and the upstream node has a sample rate of 0.75 but we have a sample rate of 0.25 (and assuming I've understood the requirements) I think we need to sample out in this case and put our own sample-rate (0.25) on the header... so we just overwrite in this scenario.
    • I'm not 100% sure whether that's how sampling is implemented at Sentry though... maybe we only support different sample rates on different nodes via the TracesSampler function. @cleptric do you know?
  • If context.IsSampled == true I think that means the SDK user has forced sampling to true (passing it explicitly via the call to StartTransaction).
    • Note that we do sometimes set this to false ourselves internally (e.g. here and here)
    • This is what I think I got us confused about. I suggested this field might be true when the upstream span was sampled in... with fresh eyes, I think I was mistaken and that's what the IsParentSampled field is for.

On the bright side, if all of the above is correct, it means it should be trivial to implement the logic to force sampling (per the other SDKs)... basically we can force a sample-rate of 0.0 or 1.0 if context.IsSampled is set.

Copy link
Member Author

Choose a reason for hiding this comment

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

followed-up via #4386

{
// Ensure the actual sampleRate is set on the provided DSC (if any) when not IsSampled upstream but the TracesSampleRate reached a sampling decision
dynamicSamplingContext = dynamicSamplingContext?.WithSampleRate(_options.TracesSampleRate.Value);
}
}

// Make sure there is a replayId (if available) on the provided DSC (if any).
dynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession);
Expand Down
48 changes: 31 additions & 17 deletions test/Sentry.Tests/HubTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -715,19 +715,19 @@ public void StartTransaction_DynamicSamplingContextWithSampleRate_UsesSampleRate
}

[SkippableTheory]
[InlineData(null, 0.3, 0.4, true, 0.3)]
[InlineData(null, 0.3, null, true, 0.3)]
[InlineData(null, null, 0.4, true, 0.4)]
[InlineData(null, null, null, false, 0.0)]
[InlineData(true, 0.3, 0.4, true, 0.3)]
[InlineData(true, 0.3, null, true, 0.3)]
[InlineData(true, null, 0.4, true, 0.4)]
[InlineData(true, null, null, true, 0.0)]
[InlineData(false, 0.3, 0.4, true, 0.3)]
[InlineData(false, 0.3, null, true, 0.3)]
[InlineData(false, null, 0.4, false, 0.4)]
[InlineData(false, null, null, false, 0.0)]
public void StartTransaction_DynamicSamplingContextWithSampleRate_OverwritesSampleRate(bool? isSampled, double? tracesSampler, double? tracesSampleRate, bool expectedIsSampled, double expectedSampleRate)
[InlineData(null, 0.3, 0.4, true, 0.3, true)]
[InlineData(null, 0.3, null, true, 0.3, true)]
[InlineData(null, null, 0.4, true, 0.4, true)]
[InlineData(null, null, null, false, 0.0, false)]
[InlineData(true, 0.3, 0.4, true, 0.3, true)]
[InlineData(true, 0.3, null, true, 0.3, true)]
[InlineData(true, null, 0.4, true, 0.4, false)]
[InlineData(true, null, null, true, 0.0, false)]
[InlineData(false, 0.3, 0.4, true, 0.3, true)]
[InlineData(false, 0.3, null, true, 0.3, true)]
[InlineData(false, null, 0.4, false, 0.4, false)]
[InlineData(false, null, null, false, 0.0, false)]
public void StartTransaction_DynamicSamplingContextWithSampleRate_OverwritesSampleRate(bool? isSampled, double? tracesSampler, double? tracesSampleRate, bool expectedIsSampled, double expectedSampleRate, bool expectedDscOverwritten)
{
#if DEBUG
Skip.If(isSampled is false && expectedIsSampled, $"Forced sampling via {nameof(ITransactionContext.IsSampled)} is not considered when {nameof(SentryOptions.TracesSampler)} is used, resulting in `Debug.Assert` of {nameof(TransactionTracer)} `.ctor`.");
Expand Down Expand Up @@ -756,15 +756,29 @@ public void StartTransaction_DynamicSamplingContextWithSampleRate_OverwritesSamp
{
var transactionTracer = transaction.Should().BeOfType<TransactionTracer>().Subject;
transactionTracer.SampleRate.Should().Be(expectedSampleRate);
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(expectedSampleRate));
if (expectedDscOverwritten)
{
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(expectedSampleRate));
}
else
{
transactionTracer.DynamicSamplingContext.Should().BeSameAs(dsc);
}
}
else
{
var unsampledTransaction = transaction.Should().BeOfType<UnsampledTransaction>().Subject;
unsampledTransaction.SampleRate.Should().Be(expectedSampleRate);
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(expectedSampleRate));
if (expectedDscOverwritten)
{
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(expectedSampleRate));
}
else
{
unsampledTransaction.DynamicSamplingContext.Should().BeSameAs(dsc);
}
}
}

Expand Down
Loading