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: DSC contains actual SampleRate
  • Loading branch information
Flash0ver committed Jul 21, 2025
commit 23a45c20af9a9ad3630d67051c807fa77c1a602a
18 changes: 18 additions & 0 deletions src/Sentry/DynamicSamplingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ private DynamicSamplingContext(SentryId traceId,

public BaggageHeader ToBaggageHeader() => BaggageHeader.Create(Items, useSentryPrefix: true);

public DynamicSamplingContext WithSampleRate(double sampleRate)
{
if (Items.TryGetValue("sample_rate", out var dscSampleRate))
{
if (double.TryParse(dscSampleRate, NumberStyles.Float, CultureInfo.InvariantCulture, out var rate))
{
if (Math.Abs(rate - sampleRate) > double.Epsilon)
{
var items = Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
items["sample_rate"] = sampleRate.ToString(CultureInfo.InvariantCulture);
return new DynamicSamplingContext(items);
}
}
}

return this;
}

public DynamicSamplingContext WithReplayId(IReplaySession? replaySession)
{
if (replaySession?.ActiveReplayId is not { } replayId || replayId == SentryId.Empty)
Expand Down
7 changes: 5 additions & 2 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ internal ITransactionTracer StartTransaction(

bool? isSampled = null;
double? sampleRate = null;
var sampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var dscsampleRand) ?? false
? double.Parse(dscsampleRand, NumberStyles.Float, CultureInfo.InvariantCulture)
var sampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var dscSampleRand) ?? false
? double.Parse(dscSampleRand, NumberStyles.Float, CultureInfo.InvariantCulture)
: SampleRandHelper.GenerateSampleRand(context.TraceId.ToString());

// TracesSampler runs regardless of whether a decision has already been made, as it can be used to override it.
Expand All @@ -196,6 +196,9 @@ internal ITransactionTracer StartTransaction(
sampleRate ??= _options.TracesSampleRate ?? 0.0;
isSampled ??= context.IsSampled ?? SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);

// Ensure the actual sampleRate is set on the provided DSC (https://github.com/getsentry/team-sdks/issues/117)
dynamicSamplingContext = dynamicSamplingContext?.WithSampleRate(sampleRate.Value);

// Make sure there is a replayId (if available) on the provided DSC (if any).
dynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession);

Expand Down
15 changes: 10 additions & 5 deletions test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ public async Task Baggage_header_propagates_to_outbound_requests(bool shouldProp
"sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8, " +
"sentry-public_key=d4d82fc1c2c4032a83f3a29aa3a3aff, " +
"sentry-sample_rand=0.1234, " +
"sentry-sample_rate=0.5";
"sentry-sample_rate=1";
}
else
{
Expand Down Expand Up @@ -395,13 +395,18 @@ public async Task Baggage_header_propagates_to_outbound_requests(bool shouldProp
[Fact]
public async Task Baggage_header_sets_dynamic_sampling_context()
{
// incoming baggage header
const string baggage =
const string incomingBaggageHeader =
"sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8, " +
"sentry-public_key=d4d82fc1c2c4032a83f3a29aa3a3aff, " +
"sentry-sample_rand=0.1234, " +
"sentry-sample_rate=0.5";

const string expectedBaggageHeader =
"sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8, " +
"sentry-public_key=d4d82fc1c2c4032a83f3a29aa3a3aff, " +
"sentry-sample_rand=0.1234, " +
"sentry-sample_rate=1";

// Arrange
TransactionTracer transaction = null;

Expand Down Expand Up @@ -440,7 +445,7 @@ public async Task Baggage_header_sets_dynamic_sampling_context()
{
Headers =
{
{"baggage", baggage}
{"baggage", incomingBaggageHeader}
}
};

Expand All @@ -449,7 +454,7 @@ public async Task Baggage_header_sets_dynamic_sampling_context()
// Assert
var dsc = transaction?.DynamicSamplingContext;
Assert.NotNull(dsc);
Assert.Equal(baggage, dsc.ToBaggageHeader().ToString());
Assert.Equal(expectedBaggageHeader, dsc.ToBaggageHeader().ToString());
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ public void OnStart_Transaction_With_DynamicSamplingContext()
{
actual.Items["trace_id"].Should().Be(expected["trace_id"]);
actual.Items["public_key"].Should().Be(expected["public_key"]);
actual.Items["sample_rate"].Should().Be(expected["sample_rate"]);
actual.Items["sample_rate"].Should().NotBe(expected["sample_rate"]);
actual.Items["sample_rate"].Should().Be(_fixture.Options.TracesSampleRate.ToString());
}
}

Expand Down
170 changes: 151 additions & 19 deletions test/Sentry.Tests/HubTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,76 @@ public void StartTransaction_SameInstrumenter_SampledIn()
transaction.IsSampled.Should().BeTrue();
}

[Fact]
public void StartTransaction_DynamicSamplingContextWithSampleRate_UsesSampleRate()
{
// Arrange
var transactionContext = new TransactionContext("name", "operation");
var dsc = BaggageHeader.Create(new List<KeyValuePair<string, string>>
{
{"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"},
{"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"},
{"sentry-sample_rate", "0.5"},
{"sentry-sample_rand", "0.1234"},
}).CreateDynamicSamplingContext();

_fixture.Options.TracesSampler = _ => 0.5;
_fixture.Options.TracesSampleRate = 0.5;

var hub = _fixture.GetSut();

// Act
var transaction = hub.StartTransaction(transactionContext, new Dictionary<string, object>(), dsc);

// Assert
var transactionTracer = transaction.Should().BeOfType<TransactionTracer>().Subject;
transactionTracer.SampleRate.Should().Be(0.5);
transactionTracer.DynamicSamplingContext.Should().BeSameAs(dsc);
}

[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void StartTransaction_DynamicSamplingContextWithSampleRate_UsesActualSampleRate(bool useTracesSampler, bool useTracesSampleRate)
{
// Arrange
var transactionContext = new TransactionContext("name", "operation");
var dsc = BaggageHeader.Create(new List<KeyValuePair<string, string>>
{
{"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"},
{"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"},
{"sentry-sample_rate", "0.5"},
{"sentry-sample_rand", "0.1234"},
}).CreateDynamicSamplingContext();

_fixture.Options.TracesSampler = useTracesSampler ? _ => 0.3 : _ => null;
_fixture.Options.TracesSampleRate = useTracesSampleRate ? 0.4 : null;

var hub = _fixture.GetSut();

// Act
var transaction = hub.StartTransaction(transactionContext, new Dictionary<string, object>(), dsc);

// Assert
if (useTracesSampler || useTracesSampleRate)
{
var expectedSampleRate = useTracesSampler ? 0.3 : 0.4;
var transactionTracer = transaction.Should().BeOfType<TransactionTracer>().Subject;
transactionTracer.SampleRate.Should().Be(expectedSampleRate);
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(expectedSampleRate));
}
else
{
var unsampledTransaction = transaction.Should().BeOfType<UnsampledTransaction>().Subject;
unsampledTransaction.SampleRate.Should().Be(0.0);
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(0));
}
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down Expand Up @@ -711,22 +781,15 @@ public void StartTransaction_DynamicSamplingContextWithReplayId_UsesActiveReplay
var transactionTracer = transaction.Should().BeOfType<TransactionTracer>().Subject;
transactionTracer.IsSampled.Should().BeTrue();
transactionTracer.DynamicSamplingContext.Should().NotBeNull();
foreach (var dscItem in dsc!.Items)

var expectedDsc = dsc.ReplaceSampleRate(_fixture.Options.TracesSampleRate.Value);
if (replaySessionIsActive)
{
if (dscItem.Key == "replay_id")
{
transactionTracer.DynamicSamplingContext!.Items["replay_id"].Should().Be(replaySessionIsActive
// We overwrite the replay_id when we have an active replay session
? _fixture.ReplaySession.ActiveReplayId.ToString()
// Otherwise we propagate whatever was in the baggage header
: dscItem.Value);
}
else
{
transactionTracer.DynamicSamplingContext!.Items.Should()
.Contain(kvp => kvp.Key == dscItem.Key && kvp.Value == dscItem.Value);
}
// We overwrite the replay_id when we have an active replay session
// Otherwise we propagate whatever was in the baggage header
expectedDsc = expectedDsc.ReplaceReplayId(_fixture.ReplaySession);
}
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(expectedDsc);
}

[Theory]
Expand Down Expand Up @@ -822,7 +885,8 @@ public void StartTransaction_DynamicSamplingContextWithSampleRand_InheritsSample
transactionTracer.IsSampled.Should().BeTrue();
transactionTracer.SampleRate.Should().Be(0.4);
transactionTracer.SampleRand.Should().Be(0.1234);
transactionTracer.DynamicSamplingContext.Should().Be(dsc);
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(0.4));
}

[Theory]
Expand Down Expand Up @@ -855,15 +919,17 @@ public void StartTransaction_TraceSampler_UsesSampleRand(double sampleRate, bool
transactionTracer.IsSampled.Should().BeTrue();
transactionTracer.SampleRate.Should().Be(sampleRate);
transactionTracer.SampleRand.Should().Be(0.1234);
transactionTracer.DynamicSamplingContext.Should().Be(dsc);
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
}
else
{
var unsampledTransaction = transaction.Should().BeOfType<UnsampledTransaction>().Subject;
unsampledTransaction.IsSampled.Should().BeFalse();
unsampledTransaction.SampleRate.Should().Be(sampleRate);
unsampledTransaction.SampleRand.Should().Be(0.1234);
unsampledTransaction.DynamicSamplingContext.Should().Be(dsc);
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
}
}

Expand Down Expand Up @@ -897,15 +963,17 @@ public void StartTransaction_StaticSampler_UsesSampleRand(double sampleRate, boo
transactionTracer.IsSampled.Should().BeTrue();
transactionTracer.SampleRate.Should().Be(sampleRate);
transactionTracer.SampleRand.Should().Be(0.1234);
transactionTracer.DynamicSamplingContext.Should().Be(dsc);
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
}
else
{
var unsampledTransaction = transaction.Should().BeOfType<UnsampledTransaction>().Subject;
unsampledTransaction.IsSampled.Should().BeFalse();
unsampledTransaction.SampleRate.Should().Be(sampleRate);
unsampledTransaction.SampleRand.Should().Be(0.1234);
unsampledTransaction.DynamicSamplingContext.Should().Be(dsc);
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
}
}

Expand Down Expand Up @@ -2108,3 +2176,67 @@ internal partial class HubTestsJsonContext : JsonSerializerContext
{
}
#endif

#nullable enable
file static class DynamicSamplingContextExtensions
{
public static DynamicSamplingContext ReplaceSampleRate(this DynamicSamplingContext? dsc, double sampleRate)
{
Assert.NotNull(dsc);

var value = sampleRate.ToString(CultureInfo.InvariantCulture);
return dsc.Replace("sentry-sample_rate", value);
}

public static DynamicSamplingContext ReplaceReplayId(this DynamicSamplingContext? dsc, IReplaySession replaySession)
{
Assert.NotNull(dsc);

if (!replaySession.ActiveReplayId.HasValue)
{
throw new InvalidOperationException($"No {nameof(IReplaySession.ActiveReplayId)}.");
}

var value = replaySession.ActiveReplayId.Value.ToString();
return dsc.Replace("sentry-replay_id", value);
}

private static DynamicSamplingContext Replace(this DynamicSamplingContext dsc, string key, string newValue)
{
var items = dsc.ToBaggageHeader().Members.ToList();
var index = items.FindSingleIndex(key);

var oldValue = items[index].Value;
if (oldValue == newValue)
{
throw new InvalidOperationException($"{key} already is {oldValue}.");
}

items[index] = new KeyValuePair<string, string>(key, newValue);

var baggage = BaggageHeader.Create(items);
var dynamicSamplingContext = DynamicSamplingContext.CreateFromBaggageHeader(baggage, null);
if (dynamicSamplingContext is null)
{
throw new InvalidOperationException($"Invalid {nameof(BaggageHeader)}: {baggage}");
}

return dynamicSamplingContext;
}

private static int FindSingleIndex(this List<KeyValuePair<string, string>> items, string key)
{
var index = items.FindIndex(item => item.Key == key);
if (index == -1)
{
throw new InvalidOperationException($"{key} not found.");
}

if (items.FindLastIndex(item => item.Key == key) != index)
{
throw new InvalidOperationException($"Duplicate {key} found.");
}

return index;
}
}