Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal static CallTargetState OnMethodBegin<TTarget>(ref string flagKey, Featu
}

var parameters = (State)state.State!;
var res = TracerManager.Instance.FeatureFlags?.Evaluate(parameters.FlagKey, parameters.TargetType, parameters.DefaultValue, parameters.TargetingKey ?? string.Empty, parameters.Attributes);
var res = TracerManager.Instance.FeatureFlags?.Evaluate(parameters.FlagKey, parameters.TargetType, parameters.DefaultValue, parameters.TargetingKey, parameters.Attributes);
return new CallTargetReturn<TReturn?>(res.DuckCast<TReturn>());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal static CallTargetState OnMethodBegin<TTarget>(ref string flagKey, Featu
}

var parameters = (State)state.State!;
var res = TracerManager.Instance.FeatureFlags?.Evaluate(parameters.FlagKey, parameters.TargetType, parameters.DefaultValue, parameters.TargetingKey ?? string.Empty, parameters.Attributes);
var res = TracerManager.Instance.FeatureFlags?.Evaluate(parameters.FlagKey, parameters.TargetType, parameters.DefaultValue, parameters.TargetingKey, parameters.Attributes);
return new CallTargetReturn<TReturn?>(res.DuckCast<TReturn>());
}

Expand Down
4 changes: 2 additions & 2 deletions tracer/src/Datadog.Trace/FeatureFlags/EvaluationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ namespace Datadog.Trace.FeatureFlags
/// <summary> Standard implementation of a EvaluationContext </summary>
/// <param name="key"> Targeting Key </param>
/// <param name="attributes"> Context optional attributes </param>
internal sealed class EvaluationContext(string key, IDictionary<string, object?>? attributes = null)
internal sealed class EvaluationContext(string? key, IDictionary<string, object?>? attributes = null)
{
/// <summary> Gets the Context Targeting Key </summary>
public string TargetingKey { get; } = key;
public string? TargetingKey { get; } = key;

/// <summary> Gets the Context optional Values </summary>
public IDictionary<string, object?> Attributes { get; } = attributes ?? new Dictionary<string, object?>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ private static bool MatchesShard(Shard shard, string? targetingKey)
[TestingAndPrivateOnly]
internal static int GetShard(string salt, string? targetingKey, int totalShards)
{
if (StringUtil.IsNullOrEmpty(targetingKey))
if (targetingKey is null)
{
throw new MissingTargetingKeyException();
}
Expand Down Expand Up @@ -417,11 +417,6 @@ internal static int GetShard(string salt, string? targetingKey, int totalShards)
// Special case "id": if not present, use targeting key
if (name == "id" && !context.Attributes.ContainsKey(name))
{
if (StringUtil.IsNullOrEmpty(context.TargetingKey))
{
throw new MissingTargetingKeyException();
}

return context.TargetingKey;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal void RegisterOnNewConfigEventHandler(Action? onNewConfig)
_onNewConfigEventHander = onNewConfig;
}

internal Evaluation Evaluate(string flagKey, ValueType resultType, object? defaultValue, string targetingKey, IDictionary<string, object?>? attributes)
internal Evaluation Evaluate(string flagKey, ValueType resultType, object? defaultValue, string? targetingKey, IDictionary<string, object?>? attributes)
{
var evaluator = Volatile.Read(ref _evaluator);
if (evaluator is null)
Expand Down
20 changes: 20 additions & 0 deletions tracer/test/Datadog.Trace.TestHelpers/FeatureFlagsHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ internal static Flag CreateTimeBasedFlag()
return new Flag { Key = "time-based-flag", Enabled = true, VariationType = ValueType.String, Variations = variants, Allocations = new List<Allocation> { alloc } };
}

internal static Flag CreateStaticFlag(string key, ValueType type, object value, string variantKey)
{
var variants = new Dictionary<string, Variant>
{
[variantKey] = new Variant { Key = variantKey, Value = value },
};

var splits = new List<Split>
{
new Split { Shards = new List<Shard>(), VariationKey = variantKey }
};

var allocations = new List<Allocation>
{
new Allocation { Key = "alloc1", Splits = splits, DoLog = false }
};

return new Flag { Key = key, Enabled = true, VariationType = type, Variations = variants, Allocations = allocations };
}

internal static Flag CreateExposureFlag()
{
var variants = new Dictionary<string, Variant>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public void EvaluateWithMissingTargetingKeyReturnsTargetingKeyMissing()
Assert.Equal(EvaluationReason.TargetingMatch, result.Reason);
Assert.Equal("on", result.Variant);

var noTargettingKeyCtx = new EvaluationContext(string.Empty); // no targetingKey
var noTargettingKeyCtx = new EvaluationContext(null); // null = missing targeting key, "" = valid
result = evaluator.Evaluate("simple-string", Trace.FeatureFlags.ValueType.String, "default", noTargettingKeyCtx);

Assert.Equal("default", result.Value);
Expand Down Expand Up @@ -465,6 +465,114 @@ public void GetLongFromMd5Tests(string salt, string targetingKey, int expected)
FeatureFlagsEvaluator.GetShard(salt, targetingKey, int.MaxValue).Should().Be(expected);
}

// ---------------------------------------------------------------------
// Null and empty targeting key tests
// ---------------------------------------------------------------------

[Fact]
public void NullTargetingKey_StaticFlag_ReturnsValue()
{
var flags = new Dictionary<string, Flag>
{
["static-flag"] = FeatureFlagsHelpers.CreateStaticFlag("static-flag", ValueType.String, "static-value", "on")
};

var evaluator = new FeatureFlagsEvaluator(null, new ServerConfiguration { Flags = flags });
var ctx = new EvaluationContext(null);

var result = evaluator.Evaluate("static-flag", Trace.FeatureFlags.ValueType.String, "default", ctx);

Assert.Equal("static-value", result.Value);
Assert.NotEqual(EvaluationReason.Error, result.Reason);
Assert.Null(result.Error);
}

[Fact]
public void NullTargetingKey_ShardedFlag_ReturnsTargetingKeyMissing()
{
var flags = new Dictionary<string, Flag>
{
["simple-string"] = FeatureFlagsHelpers.CreateSimpleFlag("simple-string", ValueType.String, "default", "on")
};

var evaluator = new FeatureFlagsEvaluator(null, new ServerConfiguration { Flags = flags });
var ctx = new EvaluationContext(null);

var result = evaluator.Evaluate("simple-string", Trace.FeatureFlags.ValueType.String, "default", ctx);

Assert.Equal("default", result.Value);
Assert.Equal(EvaluationReason.Error, result.Reason);
Assert.Equal("TARGETING_KEY_MISSING", result.Error);
}

[Fact]
public void NullTargetingKey_RuleMatchFlag_ReturnsValue()
{
var flags = new Dictionary<string, Flag>
{
["rule-based-flag"] = FeatureFlagsHelpers.CreateRuleBasedFlag()
};

var evaluator = new FeatureFlagsEvaluator(null, new ServerConfiguration { Flags = flags });
var ctx = new EvaluationContext(null, new Dictionary<string, object?> { { "email", "test@company.com" } });

var result = evaluator.Evaluate("rule-based-flag", Trace.FeatureFlags.ValueType.String, "default", ctx);

Assert.Equal("premium", result.Value);
Assert.Equal(EvaluationReason.TargetingMatch, result.Reason);
Assert.Null(result.Error);
}

[Fact]
public void NullTargetingKey_RuleWithIdAttribute_ReturnsTargetingKeyMissing()
{
// Rule that matches on "id" attribute — with null targeting key, the "id" fallback
// throws MissingTargetingKeyException because there's no targeting key to use
var variants = new Dictionary<string, Variant>
{
["matched"] = new Variant { Key = "matched", Value = "matched-value" },
};

var conditions = new List<ConditionConfiguration>
{
new ConditionConfiguration { Operator = ConditionOperator.MATCHES, Attribute = "id", Value = ".*" },
};

var rules = new List<Rule> { new Rule(conditions) };
var splits = new List<Split> { new Split { Shards = new List<Shard>(), VariationKey = "matched" } };
var alloc = new Allocation { Key = "id-alloc", Rules = rules, Splits = splits, DoLog = false };
var flag = new Flag { Key = "id-rule-flag", Enabled = true, VariationType = ValueType.String, Variations = variants, Allocations = new List<Allocation> { alloc } };

var flags = new Dictionary<string, Flag> { ["id-rule-flag"] = flag };
var evaluator = new FeatureFlagsEvaluator(null, new ServerConfiguration { Flags = flags });
var ctx = new EvaluationContext(null);

var result = evaluator.Evaluate("id-rule-flag", Trace.FeatureFlags.ValueType.String, "default", ctx);

Assert.Equal("default", result.Value);
Assert.Equal(EvaluationReason.Error, result.Reason);
Assert.Equal("TARGETING_KEY_MISSING", result.Error);
}

[Fact]
public void EmptyStringTargetingKey_ShardedFlag_ReturnsValue()
{
var flags = new Dictionary<string, Flag>
{
["simple-string"] = FeatureFlagsHelpers.CreateSimpleFlag("simple-string", ValueType.String, "sharded-value", "on")
};

var evaluator = new FeatureFlagsEvaluator(null, new ServerConfiguration { Flags = flags });
var ctx = new EvaluationContext(string.Empty);

var result = evaluator.Evaluate("simple-string", Trace.FeatureFlags.ValueType.String, "fallback", ctx);

Assert.Equal("sharded-value", result.Value);
Assert.NotEqual(EvaluationReason.Error, result.Reason);
Assert.Equal("on", result.Variant);
Assert.Null(result.Error);
}

private static Flag CreateTimeBasedFlagWithDates(string key, string startAt, string endAt)
{
var variants = new Dictionary<string, Variant>
Expand Down
Loading