Skip to content

Commit e9b3962

Browse files
chore: Made DynamicSampleContext mutable
Resolves #4381 - #4381 #skip-changelog
1 parent 087408c commit e9b3962

File tree

3 files changed

+44
-99
lines changed

3 files changed

+44
-99
lines changed

src/Sentry/DynamicSamplingContext.cs

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ namespace Sentry;
99
/// <seealso href="https://develop.sentry.dev/sdk/performance/dynamic-sampling-context"/>
1010
internal class DynamicSamplingContext
1111
{
12-
public IReadOnlyDictionary<string, string> Items { get; }
12+
public Dictionary<string, string> Items { get; }
1313

1414
public bool IsEmpty => Items.Count == 0;
1515

16-
private DynamicSamplingContext(IReadOnlyDictionary<string, string> items) => Items = items;
16+
private DynamicSamplingContext(Dictionary<string, string> items) => Items = items;
1717

1818
/// <summary>
1919
/// Gets an empty <see cref="DynamicSamplingContext"/> that can be used to "freeze" the DSC on a transaction.
2020
/// </summary>
21-
public static readonly DynamicSamplingContext Empty = new(new Dictionary<string, string>().AsReadOnly());
21+
public static DynamicSamplingContext Empty => new(new Dictionary<string, string>());
2222

2323
private DynamicSamplingContext(SentryId traceId,
2424
string publicKey,
@@ -98,34 +98,19 @@ private DynamicSamplingContext(SentryId traceId,
9898

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

101-
public DynamicSamplingContext WithSampleRate(double sampleRate)
101+
public void SetSampleRate(double sampleRate)
102102
{
103-
if (Items.TryGetValue("sample_rate", out var dscSampleRate))
104-
{
105-
if (double.TryParse(dscSampleRate, NumberStyles.Float, CultureInfo.InvariantCulture, out var rate))
106-
{
107-
if (Math.Abs(rate - sampleRate) > double.Epsilon)
108-
{
109-
var items = Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
110-
items["sample_rate"] = sampleRate.ToString(CultureInfo.InvariantCulture);
111-
return new DynamicSamplingContext(items);
112-
}
113-
}
114-
}
115-
116-
return this;
103+
Items["sample_rate"] = sampleRate.ToString(CultureInfo.InvariantCulture);
117104
}
118105

119-
public DynamicSamplingContext WithReplayId(IReplaySession? replaySession)
106+
internal DynamicSamplingContext Clone() => new(new Dictionary<string, string>(Items));
107+
108+
public void SetReplayId(IReplaySession? replaySession)
120109
{
121-
if (replaySession?.ActiveReplayId is not { } replayId || replayId == SentryId.Empty)
110+
if (replaySession?.ActiveReplayId is { } replayId && replayId != SentryId.Empty)
122111
{
123-
return this;
112+
Items["replay_id"] = replayId.ToString();
124113
}
125-
126-
var items = Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
127-
items["replay_id"] = replayId.ToString();
128-
return new DynamicSamplingContext(items);
129114
}
130115

131116
public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage, IReplaySession? replaySession)

src/Sentry/Internal/Hub.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ internal ITransactionTracer StartTransaction(
193193
isSampled = SampleRandHelper.IsSampled(sampleRand, samplerSampleRate);
194194

195195
// Ensure the actual sampleRate is set on the provided DSC (if any) when the TracesSampler reached a sampling decision
196-
dynamicSamplingContext = dynamicSamplingContext?.WithSampleRate(samplerSampleRate);
196+
dynamicSamplingContext?.SetSampleRate(samplerSampleRate);
197197
}
198198
}
199199

@@ -207,12 +207,12 @@ internal ITransactionTracer StartTransaction(
207207
if (context.IsSampled is null && _options.TracesSampleRate is not null)
208208
{
209209
// Ensure the actual sampleRate is set on the provided DSC (if any) when not IsSampled upstream but the TracesSampleRate reached a sampling decision
210-
dynamicSamplingContext = dynamicSamplingContext?.WithSampleRate(_options.TracesSampleRate.Value);
210+
dynamicSamplingContext?.SetSampleRate(_options.TracesSampleRate.Value);
211211
}
212212
}
213213

214214
// Make sure there is a replayId (if available) on the provided DSC (if any).
215-
dynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession);
215+
dynamicSamplingContext?.SetReplayId(_replaySession);
216216

217217
if (isSampled is false)
218218
{

test/Sentry.Tests/HubTests.cs

Lines changed: 31 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ public void StartTransaction_DynamicSamplingContextWithSampleRate_OverwritesSamp
743743
{"sentry-sample_rate", "0.5"},
744744
{"sentry-sample_rand", "0.1234"},
745745
}).CreateDynamicSamplingContext();
746+
var originalDsc = dsc?.Clone();
746747

747748
_fixture.Options.TracesSampler = _ => tracesSampler;
748749
_fixture.Options.TracesSampleRate = tracesSampleRate;
@@ -759,12 +760,12 @@ public void StartTransaction_DynamicSamplingContextWithSampleRate_OverwritesSamp
759760
transactionTracer.SampleRate.Should().Be(expectedSampleRate);
760761
if (expectedDscOverwritten)
761762
{
762-
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
763-
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(expectedSampleRate));
763+
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(originalDsc);
764+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(expectedSampleRate));
764765
}
765766
else
766767
{
767-
transactionTracer.DynamicSamplingContext.Should().BeSameAs(dsc);
768+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc);
768769
}
769770
}
770771
else
@@ -773,12 +774,12 @@ public void StartTransaction_DynamicSamplingContextWithSampleRate_OverwritesSamp
773774
unsampledTransaction.SampleRate.Should().Be(expectedSampleRate);
774775
if (expectedDscOverwritten)
775776
{
776-
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
777-
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(expectedSampleRate));
777+
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(originalDsc);
778+
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(expectedSampleRate));
778779
}
779780
else
780781
{
781-
unsampledTransaction.DynamicSamplingContext.Should().BeSameAs(dsc);
782+
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc);
782783
}
783784
}
784785
}
@@ -814,12 +815,12 @@ public void StartTransaction_DynamicSamplingContextWithReplayId_UsesActiveReplay
814815
transactionTracer.IsSampled.Should().BeTrue();
815816
transactionTracer.DynamicSamplingContext.Should().NotBeNull();
816817

817-
var expectedDsc = dsc.ReplaceSampleRate(_fixture.Options.TracesSampleRate.Value);
818+
var expectedDsc = dsc.CloneWithSampleRate(_fixture.Options.TracesSampleRate.Value);
818819
if (replaySessionIsActive)
819820
{
820821
// We overwrite the replay_id when we have an active replay session
821822
// Otherwise we propagate whatever was in the baggage header
822-
expectedDsc = expectedDsc.ReplaceReplayId(_fixture.ReplaySession);
823+
expectedDsc = expectedDsc.CloneWithReplayId(_fixture.ReplaySession);
823824
}
824825
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(expectedDsc);
825826
}
@@ -905,6 +906,7 @@ public void StartTransaction_DynamicSamplingContextWithSampleRand_InheritsSample
905906
{"sentry-sample_rate", "0.5"}, // Required in the baggage header, but ignored by sampling logic
906907
{"sentry-sample_rand", "0.1234"}
907908
}).CreateDynamicSamplingContext(dummyReplaySession);
909+
var originalDsc = dsc.Clone();
908910

909911
_fixture.Options.TracesSampleRate = 0.4;
910912
var hub = _fixture.GetSut();
@@ -917,8 +919,8 @@ public void StartTransaction_DynamicSamplingContextWithSampleRand_InheritsSample
917919
transactionTracer.IsSampled.Should().BeTrue();
918920
transactionTracer.SampleRate.Should().Be(0.4);
919921
transactionTracer.SampleRand.Should().Be(0.1234);
920-
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
921-
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(0.4));
922+
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(originalDsc);
923+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(0.4));
922924
}
923925

924926
[Theory]
@@ -937,6 +939,7 @@ public void StartTransaction_TraceSampler_UsesSampleRand(double sampleRate, bool
937939
{"sentry-sample_rate", "0.5"},
938940
{"sentry-sample_rand", "0.1234"}
939941
}).CreateDynamicSamplingContext(_fixture.ReplaySession);
942+
var originalDsc = dsc.Clone();
940943

941944
_fixture.Options.TracesSampler = _ => sampleRate;
942945
var hub = _fixture.GetSut();
@@ -951,17 +954,17 @@ public void StartTransaction_TraceSampler_UsesSampleRand(double sampleRate, bool
951954
transactionTracer.IsSampled.Should().BeTrue();
952955
transactionTracer.SampleRate.Should().Be(sampleRate);
953956
transactionTracer.SampleRand.Should().Be(0.1234);
954-
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
955-
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
957+
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(originalDsc);
958+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(sampleRate));
956959
}
957960
else
958961
{
959962
var unsampledTransaction = transaction.Should().BeOfType<UnsampledTransaction>().Subject;
960963
unsampledTransaction.IsSampled.Should().BeFalse();
961964
unsampledTransaction.SampleRate.Should().Be(sampleRate);
962965
unsampledTransaction.SampleRand.Should().Be(0.1234);
963-
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
964-
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
966+
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(originalDsc);
967+
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(sampleRate));
965968
}
966969
}
967970

@@ -981,6 +984,7 @@ public void StartTransaction_StaticSampler_UsesSampleRand(double sampleRate, boo
981984
{"sentry-sample_rate", "0.5"}, // Static sampling ignores this and uses options.TracesSampleRate instead
982985
{"sentry-sample_rand", "0.1234"}
983986
}).CreateDynamicSamplingContext(dummyReplaySession);
987+
var originalDsc = dsc.Clone();
984988

985989
_fixture.Options.TracesSampleRate = sampleRate;
986990
var hub = _fixture.GetSut();
@@ -995,17 +999,17 @@ public void StartTransaction_StaticSampler_UsesSampleRand(double sampleRate, boo
995999
transactionTracer.IsSampled.Should().BeTrue();
9961000
transactionTracer.SampleRate.Should().Be(sampleRate);
9971001
transactionTracer.SampleRand.Should().Be(0.1234);
998-
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
999-
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
1002+
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(originalDsc);
1003+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(sampleRate));
10001004
}
10011005
else
10021006
{
10031007
var unsampledTransaction = transaction.Should().BeOfType<UnsampledTransaction>().Subject;
10041008
unsampledTransaction.IsSampled.Should().BeFalse();
10051009
unsampledTransaction.SampleRate.Should().Be(sampleRate);
10061010
unsampledTransaction.SampleRand.Should().Be(0.1234);
1007-
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
1008-
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
1011+
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(originalDsc);
1012+
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(sampleRate));
10091013
}
10101014
}
10111015

@@ -1255,7 +1259,7 @@ public void GetBaggage_SpanActive_ReturnsBaggageFromSpan(bool isSampled)
12551259
baggage.Members.Should().Equal([
12561260
new KeyValuePair<string, string>("sentry-trace_id", "43365712692146d08ee11a729dfbcaca"),
12571261
new KeyValuePair<string, string>("sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"),
1258-
new KeyValuePair<string, string>("sentry-sample_rate", isSampled ? "1" : "0.0"),
1262+
new KeyValuePair<string, string>("sentry-sample_rate", isSampled ? "1" : "0"),
12591263
new KeyValuePair<string, string>("sentry-sample_rand", sampleRand!.Value.ToString(CultureInfo.InvariantCulture)),
12601264
]);
12611265
}
@@ -2409,63 +2413,19 @@ internal partial class HubTestsJsonContext : JsonSerializerContext
24092413
#nullable enable
24102414
file static class DynamicSamplingContextExtensions
24112415
{
2412-
public static DynamicSamplingContext ReplaceSampleRate(this DynamicSamplingContext? dsc, double sampleRate)
2416+
public static DynamicSamplingContext CloneWithSampleRate(this DynamicSamplingContext? dsc, double sampleRate)
24132417
{
24142418
Assert.NotNull(dsc);
2415-
2416-
var value = sampleRate.ToString(CultureInfo.InvariantCulture);
2417-
return dsc.Replace("sentry-sample_rate", value);
2419+
var newDsc = dsc.Clone();
2420+
newDsc.SetSampleRate(sampleRate);
2421+
return newDsc;
24182422
}
24192423

2420-
public static DynamicSamplingContext ReplaceReplayId(this DynamicSamplingContext? dsc, IReplaySession replaySession)
2424+
public static DynamicSamplingContext CloneWithReplayId(this DynamicSamplingContext? dsc, IReplaySession replaySession)
24212425
{
24222426
Assert.NotNull(dsc);
2423-
2424-
if (!replaySession.ActiveReplayId.HasValue)
2425-
{
2426-
throw new InvalidOperationException($"No {nameof(IReplaySession.ActiveReplayId)}.");
2427-
}
2428-
2429-
var value = replaySession.ActiveReplayId.Value.ToString();
2430-
return dsc.Replace("sentry-replay_id", value);
2431-
}
2432-
2433-
private static DynamicSamplingContext Replace(this DynamicSamplingContext dsc, string key, string newValue)
2434-
{
2435-
var items = dsc.ToBaggageHeader().Members.ToList();
2436-
var index = items.FindSingleIndex(key);
2437-
2438-
var oldValue = items[index].Value;
2439-
if (oldValue == newValue)
2440-
{
2441-
throw new InvalidOperationException($"{key} already is {oldValue}.");
2442-
}
2443-
2444-
items[index] = new KeyValuePair<string, string>(key, newValue);
2445-
2446-
var baggage = BaggageHeader.Create(items);
2447-
var dynamicSamplingContext = DynamicSamplingContext.CreateFromBaggageHeader(baggage, null);
2448-
if (dynamicSamplingContext is null)
2449-
{
2450-
throw new InvalidOperationException($"Invalid {nameof(BaggageHeader)}: {baggage}");
2451-
}
2452-
2453-
return dynamicSamplingContext;
2454-
}
2455-
2456-
private static int FindSingleIndex(this List<KeyValuePair<string, string>> items, string key)
2457-
{
2458-
var index = items.FindIndex(item => item.Key == key);
2459-
if (index == -1)
2460-
{
2461-
throw new InvalidOperationException($"{key} not found.");
2462-
}
2463-
2464-
if (items.FindLastIndex(item => item.Key == key) != index)
2465-
{
2466-
throw new InvalidOperationException($"Duplicate {key} found.");
2467-
}
2468-
2469-
return index;
2427+
var newDsc = dsc.Clone();
2428+
newDsc.SetReplayId(replaySession);
2429+
return newDsc;
24702430
}
24712431
}

0 commit comments

Comments
 (0)