Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

# FFE
/utils/docker_fixtures/_mock_ffe_agentless_backend.py @DataDog/feature-flagging-and-experimentation-sdk @DataDog/system-tests-core
/utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs @DataDog/feature-flagging-and-experimentation-sdk @DataDog/system-tests-core
/utils/build/docker/dotnet/weblog/Controllers/FeatureFlagEvaluatorController.cs @DataDog/feature-flagging-and-experimentation-sdk @DataDog/system-tests-core
/tests/ffe/ @DataDog/feature-flagging-and-experimentation-sdk
/tests/test_the_test/test_mock_ffe_agentless_backend.py @DataDog/feature-flagging-and-experimentation-sdk
/tests/parametric/test_ffe/ @DataDog/feature-flagging-and-experimentation-sdk
Expand Down
12 changes: 11 additions & 1 deletion manifests/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,17 @@ manifest:
tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Ignore: v3.10.0
tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage)
tests/parametric/test_extract_behavior.py::Test_ExtractBehavior_Restart_With_Extract_First: incomplete_test_app (The parametric test app does not emit restart span links or preserve baggage)
tests/parametric/test_ffe/test_configuration_sources.py: missing_feature (FFL-2703 tracks .NET agentless configuration-source implementation; FFL-2731 tracks system-tests configuration-source contract)
tests/parametric/test_ffe/test_configuration_sources.py: '>=3.54.0' # FFL-2703
? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_bad_to_unchanged_cold_preserves_not_ready
: bug (FFL-3325)
? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_malformed_cold
: bug (FFL-3325)
? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_request_timeout_cold
: bug (FFL-3325)
? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Cold_Failure_And_Recovery::test_unauthorized_cold
: bug (FFL-3325)
? tests/parametric/test_ffe/test_configuration_sources.py::Test_Feature_Flag_Configuration_Source_Selection::test_remote_config_without_rc_does_not_fallback_to_agentless
: bug (FFL-3325)
tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation: '>=3.36.0' # Modified by easy win activation script
tests/parametric/test_ffe/test_dynamic_evaluation.py::Test_Feature_Flag_Dynamic_Evaluation::test_ffe_flag_evaluation: missing_feature # Created by easy win activation script
tests/parametric/test_ffe/test_span_enrichment.py: missing_feature
Expand Down
1 change: 1 addition & 0 deletions utils/build/docker/dotnet/parametric/ApmTestApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

<PackageReference Include="Datadog.Trace" Version="*" />
<PackageReference Include="Datadog.FeatureFlags.OpenFeature" Version="*" />
</ItemGroup>

</Project>
110 changes: 110 additions & 0 deletions utils/build/docker/dotnet/parametric/Endpoints/FfeTestApi.cs
Comment thread
pavlokhrebto marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System.ComponentModel;
using System.Reflection;
using System.Text.Json;
using OpenFeature;
using OpenFeature.Constant;
using OpenFeature.Model;

namespace ApmTestApi.Endpoints;

public abstract class FfeTestApi
{
private static FeatureClient? _client;
private static ILogger? _logger;

public static void MapFfeEndpoints(WebApplication app, ILogger logger)
{
_logger = logger;

app.MapPost("/ffe/start", StartFfe);
app.MapPost("/ffe/evaluate", EvaluateFfe);
}

private static async Task<IResult> StartFfe()
{
try
{
_logger?.LogInformation("Initializing FFE provider");

var provider = new Datadog.FeatureFlags.OpenFeature.DatadogProvider();

// Only the async call runs InitializeAsync, which waits for the first configuration.
await Api.Instance.SetProviderAsync(provider);
_client = Api.Instance.GetClient();

return Results.Ok();
}
catch (Exception e)
{
_logger?.LogError(e, "Error starting FFE provider");
return Results.Json(new { error = e.Message }, statusCode: 500);
}
}

private static async Task<IResult> EvaluateFfe(EvaluateRequest request)
{
if (_client is null)
{
return Results.Json(new { error = "FFE provider not initialized" }, statusCode: 500);
}

var contextBuilder = EvaluationContext.Builder().SetTargetingKey(request.TargetingKey);

foreach (var (key, attribute) in request.Attributes ?? new())
{
switch (attribute.ValueKind)
{
case JsonValueKind.String:
contextBuilder.Set(key, attribute.GetString()!);
break;
case JsonValueKind.Number:
contextBuilder.Set(key, attribute.GetDouble());
break;
case JsonValueKind.True:
case JsonValueKind.False:
contextBuilder.Set(key, attribute.GetBoolean());
break;
default:
contextBuilder.Set(key, attribute.GetRawText());
break;
}
}

var context = contextBuilder.Build();
var defaultValue = request.DefaultValue;

try
{
return request.VariationType switch
{
"BOOLEAN" => ToResult(await _client.GetBooleanDetailsAsync(request.Flag, defaultValue.GetBoolean(), context)),
"STRING" => ToResult(await _client.GetStringDetailsAsync(request.Flag, defaultValue.GetString()!, context)),
"INTEGER" => ToResult(await _client.GetIntegerDetailsAsync(request.Flag, defaultValue.GetInt32(), context)),
"NUMERIC" => ToResult(await _client.GetDoubleDetailsAsync(request.Flag, defaultValue.GetDouble(), context)),
"JSON" => ToResult(await _client.GetObjectDetailsAsync(request.Flag, new Value(defaultValue.GetRawText()), context)),
_ => Results.Ok(new { value = defaultValue, reason = "DEFAULT", errorCode = (string?)null }),
};
}
catch (Exception e)
{
_logger?.LogError(e, "Error evaluating flag");
return Results.Ok(new { value = defaultValue, reason = "ERROR", errorCode = (string?)null });
}
}

private static IResult ToResult<T>(FlagEvaluationDetails<T> details)
=> Results.Ok(new { value = details.Value, reason = details.Reason ?? "DEFAULT", errorCode = ToErrorCode(details.ErrorType) });

// OpenFeature carries the spec error code on each ErrorType member as its Description.
private static string? ToErrorCode(ErrorType errorType)
=> errorType == ErrorType.None
? null
: typeof(ErrorType).GetField(errorType.ToString())?.GetCustomAttribute<DescriptionAttribute>()?.Description;

public sealed record EvaluateRequest(
string Flag,
string VariationType,
JsonElement DefaultValue,
string TargetingKey,
Dictionary<string, JsonElement>? Attributes);
}
1 change: 1 addition & 0 deletions utils/build/docker/dotnet/parametric/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// Map endpoints
ApmTestApi.Endpoints.ApmTestApi.MapApmTraceEndpoints(app, logger);
ApmTestApi.Endpoints.ApmTestApiOtel.MapApmOtelEndpoints(app, otelLogger);
ApmTestApi.Endpoints.FfeTestApi.MapFfeEndpoints(app, logger);

if (!int.TryParse(Environment.GetEnvironmentVariable("APM_TEST_CLIENT_SERVER_PORT"), out var port))
{
Expand Down
Loading