Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
sample_rand now added to DSC from BaggageHeaders
  • Loading branch information
jamescrosswell committed Feb 11, 2025
commit 3e7034d2571b440c44530b9a992e35d7ee5674d9
2 changes: 1 addition & 1 deletion src/Sentry/BaggageHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private BaggageHeader(IEnumerable<KeyValuePair<string, string>> members) =>

// We can safely return a dictionary of Sentry members, as we are in control over the keys added.
// Just to be safe though, we'll group by key and only take the first of each one.
internal IReadOnlyDictionary<string, string> GetSentryMembers() =>
internal Dictionary<string, string> GetSentryMembers() =>
Members
.Where(kvp => kvp.Key.StartsWith(SentryKeyPrefix))
.GroupBy(kvp => kvp.Key, kvp => kvp.Value)
Expand Down
16 changes: 16 additions & 0 deletions src/Sentry/DynamicSamplingContext.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Sentry.Internal;
using Sentry.Internal.Extensions;

namespace Sentry;
Expand Down Expand Up @@ -111,6 +112,21 @@ private DynamicSamplingContext(
return null;
}

// See https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value
if (items.TryGetValue("sample_rand", out var sampleRand))
Copy link
Member

Choose a reason for hiding this comment

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

odd that we validate this to be within a range as a float but then we make it a string then try to parse it back out.
Can we rely on the original value and avoid the parsing instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure I follow. It may or may not be present as a string in the baggage header. If it's present then here we try to cast it to a float and ensure it's in the correct range.

If it's present but malformed then we don't create the DSC from the baggage header.

{
if (!double.TryParse(sampleRand, NumberStyles.Float, CultureInfo.InvariantCulture, out var rand) ||
rand is < 0.0 or >= 1.0)
Copy link
Member

Choose a reason for hiding this comment

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

didn't we validate this above before adding it to items? or it could already be in items when the method was caled? in which case wouldn't we be adding it twice? (I add Add so assume it's not a dictionary)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This method is creating a DSC from the baggage header. The BaggageHeader is a pretty simple class that just pulls stuff directly off a certain HTTP header, without any strong opinions about what the items in that header should be. Other vendors might be putting things on that header so we just propagate it, pretty much verbatim (we might add things to it but we never remove things from it).

But we can't control what will be on that baggage header in this SDK. A request could be made to our SDK with a malformed sentry-sample_rand item in the BaggageHeader.

{
return null;
}
}
else
{
var seed = FnvHash.ComputeHash(traceId);
var rand = new Random(seed).NextDouble() * rate;
items.Add("sample_rand", rand.ToString("N4", CultureInfo.InvariantCulture));
}
return new DynamicSamplingContext(items);
}

Expand Down
100 changes: 100 additions & 0 deletions test/Sentry.Tests/DynamicSamplingContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,104 @@ public void CreateFromBaggage_SampleRate_TooHigh()
Assert.Null(dsc);
}

[Fact]
public void CreateFromBaggage_SampleRand_Invalid()
{
var baggage = BaggageHeader.Create(new List<KeyValuePair<string, string>>
{
{"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"},
{"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"},
{"sentry-sample_rate", "1.0"},
{"sentry-sample_rand", "not-a-number"},
});

var dsc = baggage.CreateDynamicSamplingContext();

Assert.Null(dsc);
}

[Fact]
public void CreateFromBaggage_SampleRand_TooLow()
{
var baggage = BaggageHeader.Create(new List<KeyValuePair<string, string>>
{
{"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"},
{"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"},
{"sentry-sample_rate", "1.0"},
{"sentry-sample_rand", "-0.1"}
});

var dsc = baggage.CreateDynamicSamplingContext();

Assert.Null(dsc);
}

[Fact]
public void CreateFromBaggage_SampleRand_TooHigh()
{
var baggage = BaggageHeader.Create(new List<KeyValuePair<string, string>>
{
{"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"},
{"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"},
{"sentry-sample_rate", "1.0"},
{"sentry-sample_rand", "1.0"} // Must be less than 1
});

var dsc = baggage.CreateDynamicSamplingContext();

Assert.Null(dsc);
}

[Fact]
public void CreateFromBaggage_NotSampledNoSampleRand_GeneratesSampleRand()
{
var baggage = BaggageHeader.Create(new List<KeyValuePair<string, string>>
{
{"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"},
{"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"},
{"sentry-sample_rate", "0.5"}
});

var dsc = baggage.CreateDynamicSamplingContext();

using var scope = new AssertionScope();
Assert.NotNull(dsc);
var sampleRandItem = Assert.Contains("sample_rand", dsc.Items);
var sampleRand = double.Parse(sampleRandItem, NumberStyles.Float, CultureInfo.InvariantCulture);
Assert.True(sampleRand >= 0.0);
Assert.True(sampleRand < 1.0);
}

[Theory]
[InlineData("true")]
public void CreateFromBaggage_SampledNoSampleRand_GeneratesConsistentSampleRand(string sampled)
{
var baggage = BaggageHeader.Create(new List<KeyValuePair<string, string>>
{
{"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"},
{"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"},
{"sentry-sample_rate", "0.5"},
{"sentry-sampled", sampled},
});

var dsc = baggage.CreateDynamicSamplingContext();

using var scope = new AssertionScope();
Assert.NotNull(dsc);
var sampleRandItem = Assert.Contains("sample_rand", dsc.Items);
var sampleRand = double.Parse(sampleRandItem, NumberStyles.Float, CultureInfo.InvariantCulture);
if (sampled == "true")
{
Assert.True(sampleRand >= 0.0);
Assert.True(sampleRand < 0.5);
}
else
{
Assert.True(sampleRand >= 0.5);
Assert.True(sampleRand < 1.0);
}
}

[Fact]
public void CreateFromBaggage_Sampled_MalFormed()
{
Expand Down Expand Up @@ -186,6 +284,7 @@ public void CreateFromBaggage_Valid_Complete()
{"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"},
{"sentry-sampled", "true"},
{"sentry-sample_rate", "1.0"},
{"sentry-sample_rand", "0.1234"},
{"sentry-release", "[email protected]+abc"},
{"sentry-environment", "production"},
{"sentry-user_segment", "Group B"},
Expand All @@ -200,6 +299,7 @@ public void CreateFromBaggage_Valid_Complete()
Assert.Equal("d4d82fc1c2c4032a83f3a29aa3a3aff", Assert.Contains("public_key", dsc.Items));
Assert.Equal("true", Assert.Contains("sampled", dsc.Items));
Assert.Equal("1.0", Assert.Contains("sample_rate", dsc.Items));
Assert.Equal("0.1234", Assert.Contains("sample_rand", dsc.Items));
Assert.Equal("[email protected]+abc", Assert.Contains("release", dsc.Items));
Assert.Equal("production", Assert.Contains("environment", dsc.Items));
Assert.Equal("Group B", Assert.Contains("user_segment", dsc.Items));
Expand Down