Skip to content

Commit e0ed84c

Browse files
Propagate Sampling Seed (#3951)
1 parent 1f9d4f2 commit e0ed84c

19 files changed

+391
-15
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- The sample seed used for sampling decisions is now propagated, for use in downstream custom trace samplers ([#3951](https://github.com/getsentry/sentry-dotnet/pull/3951))
8+
59
### Fixes
10+
611
- Add Azure Function UseSentry overloads for easier wire ups ([#3971](https://github.com/getsentry/sentry-dotnet/pull/3971))
712

813
### Dependencies

src/Sentry/BaggageHeader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ private BaggageHeader(IEnumerable<KeyValuePair<string, string>> members) =>
2626

2727
// We can safely return a dictionary of Sentry members, as we are in control over the keys added.
2828
// Just to be safe though, we'll group by key and only take the first of each one.
29-
internal IReadOnlyDictionary<string, string> GetSentryMembers() =>
29+
internal Dictionary<string, string> GetSentryMembers() =>
3030
Members
3131
.Where(kvp => kvp.Key.StartsWith(SentryKeyPrefix))
3232
.GroupBy(kvp => kvp.Key, kvp => kvp.Value)

src/Sentry/DynamicSamplingContext.cs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Sentry.Internal;
12
using Sentry.Internal.Extensions;
23

34
namespace Sentry;
@@ -24,27 +25,33 @@ private DynamicSamplingContext(
2425
string publicKey,
2526
bool? sampled,
2627
double? sampleRate = null,
28+
double? sampleRand = null,
2729
string? release = null,
2830
string? environment = null,
2931
string? transactionName = null)
3032
{
3133
// Validate and set required values
3234
if (traceId == SentryId.Empty)
3335
{
34-
throw new ArgumentOutOfRangeException(nameof(traceId));
36+
throw new ArgumentOutOfRangeException(nameof(traceId), "cannot be empty");
3537
}
3638

3739
if (string.IsNullOrWhiteSpace(publicKey))
3840
{
39-
throw new ArgumentException(default, nameof(publicKey));
41+
throw new ArgumentException("cannot be empty", nameof(publicKey));
4042
}
4143

4244
if (sampleRate is < 0.0 or > 1.0)
4345
{
44-
throw new ArgumentOutOfRangeException(nameof(sampleRate));
46+
throw new ArgumentOutOfRangeException(nameof(sampleRate), "Arg invalid if < 0.0 or > 1.0");
4547
}
4648

47-
var items = new Dictionary<string, string>(capacity: 7)
49+
if (sampleRand is < 0.0 or >= 1.0)
50+
{
51+
throw new ArgumentOutOfRangeException(nameof(sampleRand), "Arg invalid if < 0.0 or >= 1.0");
52+
}
53+
54+
var items = new Dictionary<string, string>(capacity: 8)
4855
{
4956
["trace_id"] = traceId.ToString(),
5057
["public_key"] = publicKey,
@@ -61,6 +68,11 @@ private DynamicSamplingContext(
6168
items.Add("sample_rate", sampleRate.Value.ToString(CultureInfo.InvariantCulture));
6269
}
6370

71+
if (sampleRand is not null)
72+
{
73+
items.Add("sample_rand", sampleRand.Value.ToString("N4", CultureInfo.InvariantCulture));
74+
}
75+
6476
if (!string.IsNullOrWhiteSpace(release))
6577
{
6678
items.Add("release", release);
@@ -99,7 +111,7 @@ private DynamicSamplingContext(
99111
return null;
100112
}
101113

102-
if (items.TryGetValue("sampled", out var sampledString) && !bool.TryParse(sampledString, out _))
114+
if (items.TryGetValue("sampled", out var sampledString) && !bool.TryParse(sampledString, out var sampled))
103115
{
104116
return null;
105117
}
@@ -111,6 +123,27 @@ private DynamicSamplingContext(
111123
return null;
112124
}
113125

126+
// See https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value
127+
if (items.TryGetValue("sample_rand", out var sampleRand))
128+
{
129+
if (!double.TryParse(sampleRand, NumberStyles.Float, CultureInfo.InvariantCulture, out var rand) ||
130+
rand is < 0.0 or >= 1.0)
131+
{
132+
return null;
133+
}
134+
}
135+
else
136+
{
137+
var rand = SampleRandHelper.GenerateSampleRand(traceId);
138+
if (!string.IsNullOrEmpty(sampledString))
139+
{
140+
// Ensure sample_rand is consistent with the sampling decision that has already been made
141+
rand = bool.Parse(sampledString)
142+
? rand * rate // 0 <= sampleRand < rate
143+
: rate + (1 - rate) * rand; // rate < sampleRand < 1
144+
}
145+
items.Add("sample_rand", rand.ToString("N4", CultureInfo.InvariantCulture));
146+
}
114147
return new DynamicSamplingContext(items);
115148
}
116149

@@ -121,6 +154,7 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra
121154
var traceId = transaction.TraceId;
122155
var sampled = transaction.IsSampled;
123156
var sampleRate = transaction.SampleRate!.Value;
157+
var sampleRand = transaction.SampleRand;
124158
var transactionName = transaction.NameSource.IsHighQuality() ? transaction.Name : null;
125159

126160
// These two may not have been set yet on the transaction, but we can get them directly.
@@ -132,6 +166,7 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra
132166
publicKey,
133167
sampled,
134168
sampleRate,
169+
sampleRand,
135170
release,
136171
environment,
137172
transactionName);

src/Sentry/Internal/FnvHash.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
namespace Sentry.Internal;
2+
3+
/// <summary>
4+
/// FNV is a non-cryptographic hash.
5+
///
6+
/// See https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV_hash_parameters
7+
/// </summary>
8+
/// <remarks>
9+
/// We use a struct to avoid heap allocations.
10+
/// </remarks>
11+
internal struct FnvHash
12+
{
13+
public FnvHash()
14+
{
15+
}
16+
17+
private const int Offset = unchecked((int)2166136261);
18+
private const int Prime = 16777619;
19+
20+
private int HashCode { get; set; } = Offset;
21+
22+
private void Combine(byte data)
23+
{
24+
unchecked
25+
{
26+
HashCode ^= data;
27+
HashCode *= Prime;
28+
}
29+
}
30+
31+
private static int ComputeHash(byte[] data)
32+
{
33+
var result = new FnvHash();
34+
foreach (var b in data)
35+
{
36+
result.Combine(b);
37+
}
38+
39+
return result.HashCode;
40+
}
41+
42+
public static int ComputeHash(string data) => ComputeHash(Encoding.UTF8.GetBytes(data));
43+
}

src/Sentry/Internal/Hub.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,12 @@ internal ITransactionTracer StartTransaction(
127127
IReadOnlyDictionary<string, object?> customSamplingContext,
128128
DynamicSamplingContext? dynamicSamplingContext)
129129
{
130-
var transaction = new TransactionTracer(this, context);
130+
var transaction = new TransactionTracer(this, context)
131+
{
132+
SampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var sampleRand) ?? false
133+
? double.Parse(sampleRand, NumberStyles.Float, CultureInfo.InvariantCulture)
134+
: SampleRandHelper.GenerateSampleRand(context.TraceId.ToString())
135+
};
131136

132137
// If the hub is disabled, we will always sample out. In other words, starting a transaction
133138
// after disposing the hub will result in that transaction not being sent to Sentry.
@@ -151,7 +156,7 @@ internal ITransactionTracer StartTransaction(
151156

152157
if (tracesSampler(samplingContext) is { } sampleRate)
153158
{
154-
transaction.IsSampled = _randomValuesFactory.NextBool(sampleRate);
159+
transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate);
155160
transaction.SampleRate = sampleRate;
156161
}
157162
}
@@ -160,7 +165,7 @@ internal ITransactionTracer StartTransaction(
160165
if (transaction.IsSampled == null)
161166
{
162167
var sampleRate = _options.TracesSampleRate ?? 0.0;
163-
transaction.IsSampled = _randomValuesFactory.NextBool(sampleRate);
168+
transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate);
164169
transaction.SampleRate = sampleRate;
165170
}
166171

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Sentry.Internal;
2+
3+
internal static class SampleRandHelper
4+
{
5+
internal static double GenerateSampleRand(string traceId)
6+
=> new Random(FnvHash.ComputeHash(traceId)).NextDouble();
7+
8+
internal static bool IsSampled(double sampleRand, double rate) => rate switch
9+
{
10+
>= 1 => true,
11+
<= 0 => false,
12+
_ => sampleRand < rate
13+
};
14+
15+
}

src/Sentry/TransactionTracer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ internal set
100100
/// </summary>
101101
public double? SampleRate { get; internal set; }
102102

103+
internal double? SampleRand { get; set; }
104+
103105
/// <inheritdoc />
104106
public SentryLevel? Level { get; set; }
105107

test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#if NETCOREAPP3_1_OR_GREATER
21
using Microsoft.AspNetCore.Builder;
32
using Microsoft.AspNetCore.Hosting;
43
using Microsoft.AspNetCore.Http;
@@ -285,6 +284,7 @@ public async Task Baggage_header_propagates_to_outbound_requests(bool shouldProp
285284
const string incomingBaggage =
286285
"sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8, " +
287286
"sentry-public_key=d4d82fc1c2c4032a83f3a29aa3a3aff, " +
287+
"sentry-sample_rand=0.1234, " +
288288
"sentry-sample_rate=0.5, " +
289289
"foo-bar=abc123";
290290

@@ -299,6 +299,7 @@ public async Task Baggage_header_propagates_to_outbound_requests(bool shouldProp
299299
"other-value=abc123, " +
300300
"sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8, " +
301301
"sentry-public_key=d4d82fc1c2c4032a83f3a29aa3a3aff, " +
302+
"sentry-sample_rand=0.1234, " +
302303
"sentry-sample_rate=0.5";
303304
}
304305
else
@@ -382,6 +383,7 @@ public async Task Baggage_header_sets_dynamic_sampling_context()
382383
const string baggage =
383384
"sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8, " +
384385
"sentry-public_key=d4d82fc1c2c4032a83f3a29aa3a3aff, " +
386+
"sentry-sample_rand=0.1234, " +
385387
"sentry-sample_rate=0.5";
386388

387389
// Arrange
@@ -677,5 +679,3 @@ public async Task Transaction_TransactionNameProviderSetUnset_TransactionNameSet
677679
transaction.NameSource.Should().Be(TransactionNameSource.Url);
678680
}
679681
}
680-
681-
#endif

test/Sentry.Testing/VerifyExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public static SettingsTask IgnoreStandardSentryMembers(this SettingsTask setting
1111
return settings
1212
.ScrubMachineName()
1313
.ScrubUserName()
14+
.ScrubMember("sample_rand")
1415
.AddExtraSettings(_ =>
1516
{
1617
_.Converters.Add(new SpansConverter());

0 commit comments

Comments
 (0)