diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3e870319..9b184d3f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -16,6 +16,7 @@
+
@@ -29,6 +30,8 @@
+
+
@@ -39,4 +42,4 @@
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index 9288edfd..e87f2584 100644
--- a/README.md
+++ b/README.md
@@ -79,23 +79,23 @@ public async Task Example()
The [`samples/`](./samples) folder contains example applications demonstrating how to use OpenFeature in different .NET scenarios.
-| Sample Name | Description |
-|---------------------------------------------------|----------------------------------------------------------------|
-| [AspNetCore](/samples/AspNetCore/README.md) | Feature flags in an ASP.NET Core Web API. |
+| Sample Name | Description |
+| ------------------------------------------- | ----------------------------------------- |
+| [AspNetCore](/samples/AspNetCore/README.md) | Feature flags in an ASP.NET Core Web API. |
**Getting Started with a Sample:**
1. Navigate to the sample directory
- ```shell
- cd samples/AspNetCore
- ```
+ ```shell
+ cd samples/AspNetCore
+ ```
2. Restore dependencies and run
- ```shell
- dotnet run
- ```
+ ```shell
+ dotnet run
+ ```
Want to contribute a new sample? See our [CONTRIBUTING](CONTRIBUTING.md) guide!
@@ -534,6 +534,135 @@ services.AddOpenFeature(builder =>
});
```
+### Trace Enricher Hook
+
+The `TraceEnricherHook` enriches telemetry traces with additional information during the feature flag evaluation lifecycle. This hook adds relevant flag evaluation details as tags and events to the current `Activity` for tracing purposes.
+
+For this hook to function correctly, an active span must be set in the current `Activity`, otherwise the hook will no-op.
+
+Below are the tags added to the trace event:
+
+| Tag Name | Description | Source |
+| --------------------------- | ---------------------------------------------------------------------------- | ----------------------------- |
+| feature_flag.key | The lookup key of the feature flag | Hook context flag key |
+| feature_flag.provider.name | The name of the feature flag provider | Provider metadata |
+| feature_flag.result.reason | The reason code which shows how a feature flag value was determined | Evaluation details |
+| feature_flag.result.variant | A semantic identifier for an evaluated flag value | Evaluation details |
+| feature_flag.result.value | The evaluated value of the feature flag | Evaluation details |
+| feature_flag.context.id | The unique identifier for the flag evaluation context | Flag metadata (if available) |
+| feature_flag.set.id | The identifier of the flag set to which the feature flag belongs | Flag metadata (if available) |
+| feature_flag.version | The version of the ruleset used during the evaluation | Flag metadata (if available) |
+| error.type | Describes a class of error the operation ended with | Evaluation details (if error) |
+| error.message | A message explaining the nature of an error occurring during flag evaluation | Evaluation details (if error) |
+
+#### Example
+
+The following example demonstrates the use of the `TraceEnricherHook` with the `OpenFeature dotnet-sdk`. The traces are sent to a `jaeger` OTLP collector running at `localhost:4317`.
+
+```csharp
+using OpenFeature.Contrib.Providers.Flagd;
+using OpenFeature.Hooks;
+using OpenTelemetry.Exporter;
+using OpenTelemetry.Resources;
+using OpenTelemetry;
+using OpenTelemetry.Trace;
+
+namespace OpenFeatureTestApp
+{
+ class Hello {
+ static void Main(string[] args) {
+
+ // set up the OpenTelemetry OTLP exporter
+ var tracerProvider = Sdk.CreateTracerProviderBuilder()
+ .AddSource("my-tracer")
+ .ConfigureResource(r => r.AddService("jaeger-test"))
+ .AddOtlpExporter(o =>
+ {
+ o.ExportProcessorType = ExportProcessorType.Simple;
+ })
+ .Build();
+
+ // add the TraceEnricherHook to the OpenFeature instance
+ OpenFeature.Api.Instance.AddHooks(new TraceEnricherHook());
+
+ var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013"));
+
+ // Set the flagdProvider as the provider for the OpenFeature SDK
+ OpenFeature.Api.Instance.SetProvider(flagdProvider);
+
+ var client = OpenFeature.Api.Instance.GetClient("my-app");
+
+ var val = client.GetBooleanValueAsync("myBoolFlag", false, null);
+
+ // Print the value of the 'myBoolFlag' feature flag
+ System.Console.WriteLine(val.Result.ToString());
+ }
+ }
+}
+```
+
+After running this example, you will be able to see the traces, including the events sent by the hook in your Jaeger UI.
+
+### Metrics Hook
+
+For this hook to function correctly a global `MeterProvider` must be set.
+`MetricsHook` performs metric collection by tapping into various hook stages.
+
+Below are the metrics extracted by this hook and dimensions they carry:
+
+| Metric key | Description | Unit | Dimensions |
+| -------------------------------------- | ------------------------------- | ------------ | ----------------------------- |
+| feature_flag.evaluation_requests_total | Number of evaluation requests | {request} | key, provider name |
+| feature_flag.evaluation_success_total | Flag evaluation successes | {impression} | key, provider name, reason |
+| feature_flag.evaluation_error_total | Flag evaluation errors | 1 | key, provider name, exception |
+| feature_flag.evaluation_active_count | Active flag evaluations counter | 1 | key, provider name |
+
+Consider the following code example for usage.
+
+#### Example
+
+The following example demonstrates the use of the `MetricsHook` with the `OpenFeature dotnet-sdk`. The metrics are sent to the `console`.
+
+```csharp
+using OpenFeature.Contrib.Providers.Flagd;
+using OpenFeature;
+using OpenFeature.Hooks;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+
+namespace OpenFeatureTestApp
+{
+ class Hello {
+ static void Main(string[] args) {
+
+ // set up the OpenTelemetry OTLP exporter
+ var meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddMeter("OpenFeature")
+ .ConfigureResource(r => r.AddService("openfeature-test"))
+ .AddConsoleExporter()
+ .Build();
+
+ // add the MetricsHook to the OpenFeature instance
+ OpenFeature.Api.Instance.AddHooks(new MetricsHook());
+
+ var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013"));
+
+ // Set the flagdProvider as the provider for the OpenFeature SDK
+ OpenFeature.Api.Instance.SetProvider(flagdProvider);
+
+ var client = OpenFeature.Api.Instance.GetClient("my-app");
+
+ var val = client.GetBooleanValueAsync("myBoolFlag", false, null);
+
+ // Print the value of the 'myBoolFlag' feature flag
+ System.Console.WriteLine(val.Result.ToString());
+ }
+ }
+}
+```
+
+After running this example, you should be able to see some metrics being generated into the console.
+
## ⭐️ Support the project
diff --git a/samples/AspNetCore/Program.cs b/samples/AspNetCore/Program.cs
index a9c2cd50..5f4f0146 100644
--- a/samples/AspNetCore/Program.cs
+++ b/samples/AspNetCore/Program.cs
@@ -3,16 +3,33 @@
using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Hooks;
using OpenFeature.Providers.Memory;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddProblemDetails();
+// Configure OpenTelemetry
+builder.Services.AddOpenTelemetry()
+ .ConfigureResource(resource => resource.AddService("openfeature-aspnetcore-sample"))
+ .WithTracing(tracing => tracing
+ .AddAspNetCoreInstrumentation()
+ .AddOtlpExporter())
+ .WithMetrics(metrics => metrics
+ .AddAspNetCoreInstrumentation()
+ .AddMeter("OpenFeature")
+ .AddOtlpExporter());
+
builder.Services.AddOpenFeature(featureBuilder =>
{
featureBuilder.AddHostedFeatureLifecycle()
.AddHook(sp => new LoggingHook(sp.GetRequiredService>()))
+ .AddHook()
+ .AddHook()
.AddInMemoryProvider("InMemory", _ => new Dictionary()
{
{
diff --git a/samples/AspNetCore/Samples.AspNetCore.csproj b/samples/AspNetCore/Samples.AspNetCore.csproj
index 01e452d7..3dd554a7 100644
--- a/samples/AspNetCore/Samples.AspNetCore.csproj
+++ b/samples/AspNetCore/Samples.AspNetCore.csproj
@@ -1,9 +1,19 @@
+
+ false
+
+
+
+
+
+
+
+
diff --git a/src/OpenFeature/Hooks/MetricsConstants.cs b/src/OpenFeature/Hooks/MetricsConstants.cs
new file mode 100644
index 00000000..e54dd61c
--- /dev/null
+++ b/src/OpenFeature/Hooks/MetricsConstants.cs
@@ -0,0 +1,16 @@
+namespace OpenFeature.Hooks;
+
+internal static class MetricsConstants
+{
+ internal const string ActiveCountName = "feature_flag.evaluation_active_count";
+ internal const string RequestsTotalName = "feature_flag.evaluation_requests_total";
+ internal const string SuccessTotalName = "feature_flag.evaluation_success_total";
+ internal const string ErrorTotalName = "feature_flag.evaluation_error_total";
+
+ internal const string ActiveDescription = "active flag evaluations counter";
+ internal const string RequestsDescription = "feature flag evaluation request counter";
+ internal const string SuccessDescription = "feature flag evaluation success counter";
+ internal const string ErrorDescription = "feature flag evaluation error counter";
+
+ internal const string ExceptionAttr = "exception";
+}
diff --git a/src/OpenFeature/Hooks/MetricsHook.cs b/src/OpenFeature/Hooks/MetricsHook.cs
new file mode 100644
index 00000000..2f2314f0
--- /dev/null
+++ b/src/OpenFeature/Hooks/MetricsHook.cs
@@ -0,0 +1,100 @@
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using System.Reflection;
+using OpenFeature.Constant;
+using OpenFeature.Model;
+using OpenFeature.Telemetry;
+
+namespace OpenFeature.Hooks;
+
+///
+/// Represents a hook for capturing metrics related to flag evaluations.
+/// The meter instrumentation name is "OpenFeature".
+///
+/// This is still experimental and subject to change.
+public class MetricsHook : Hook
+{
+ private static readonly AssemblyName AssemblyName = typeof(MetricsHook).Assembly.GetName();
+ private static readonly string InstrumentationName = AssemblyName.Name ?? "OpenFeature";
+ private static readonly string InstrumentationVersion = AssemblyName.Version?.ToString() ?? "1.0.0";
+ private static readonly Meter Meter = new(InstrumentationName, InstrumentationVersion);
+
+ private readonly UpDownCounter _evaluationActiveUpDownCounter;
+ private readonly Counter _evaluationRequestCounter;
+ private readonly Counter _evaluationSuccessCounter;
+ private readonly Counter _evaluationErrorCounter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MetricsHook()
+ {
+ this._evaluationActiveUpDownCounter = Meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, description: MetricsConstants.ActiveDescription);
+ this._evaluationRequestCounter = Meter.CreateCounter(MetricsConstants.RequestsTotalName, "{request}", MetricsConstants.RequestsDescription);
+ this._evaluationSuccessCounter = Meter.CreateCounter(MetricsConstants.SuccessTotalName, "{impression}", MetricsConstants.SuccessDescription);
+ this._evaluationErrorCounter = Meter.CreateCounter(MetricsConstants.ErrorTotalName, description: MetricsConstants.ErrorDescription);
+ }
+
+ ///
+ public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ var tagList = new TagList
+ {
+ { TelemetryConstants.Key, context.FlagKey },
+ { TelemetryConstants.Provider, context.ProviderMetadata.Name }
+ };
+
+ this._evaluationActiveUpDownCounter.Add(1, tagList);
+ this._evaluationRequestCounter.Add(1, tagList);
+
+ return base.BeforeAsync(context, hints, cancellationToken);
+ }
+
+
+ ///
+ public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ var tagList = new TagList
+ {
+ { TelemetryConstants.Key, context.FlagKey },
+ { TelemetryConstants.Provider, context.ProviderMetadata.Name },
+ { TelemetryConstants.Reason, details.Reason ?? Reason.Unknown.ToString() }
+ };
+
+ this._evaluationSuccessCounter.Add(1, tagList);
+
+ return base.AfterAsync(context, details, hints, cancellationToken);
+ }
+
+ ///
+ public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ var tagList = new TagList
+ {
+ { TelemetryConstants.Key, context.FlagKey },
+ { TelemetryConstants.Provider, context.ProviderMetadata.Name },
+ { MetricsConstants.ExceptionAttr, error.Message }
+ };
+
+ this._evaluationErrorCounter.Add(1, tagList);
+
+ return base.ErrorAsync(context, error, hints, cancellationToken);
+ }
+
+ ///
+ public override ValueTask FinallyAsync(HookContext context,
+ FlagEvaluationDetails evaluationDetails,
+ IReadOnlyDictionary? hints = null,
+ CancellationToken cancellationToken = default)
+ {
+ var tagList = new TagList
+ {
+ { TelemetryConstants.Key, context.FlagKey },
+ { TelemetryConstants.Provider, context.ProviderMetadata.Name }
+ };
+
+ this._evaluationActiveUpDownCounter.Add(-1, tagList);
+
+ return base.FinallyAsync(context, evaluationDetails, hints, cancellationToken);
+ }
+}
diff --git a/src/OpenFeature/Hooks/TraceEnricherHook.cs b/src/OpenFeature/Hooks/TraceEnricherHook.cs
new file mode 100644
index 00000000..08914b1c
--- /dev/null
+++ b/src/OpenFeature/Hooks/TraceEnricherHook.cs
@@ -0,0 +1,38 @@
+using System.Diagnostics;
+using OpenFeature.Model;
+using OpenFeature.Telemetry;
+
+namespace OpenFeature.Hooks;
+
+///
+/// A hook that enriches telemetry traces with additional information during the feature flag evaluation lifecycle.
+/// This hook adds relevant flag evaluation details as tags and events to the current for tracing purposes.
+/// On error, it attaches exception information to the trace, using the appropriate API depending on the .NET version.
+///
+/// This is still experimental and subject to change.
+public class TraceEnricherHook : Hook
+{
+ ///
+ /// Adds tags and events to the current for tracing purposes.
+ ///
+ /// The type of the flag value being evaluated.
+ /// The hook context containing metadata about the evaluation.
+ /// Details about the flag evaluation including the key, value, and variant.
+ /// Optional dictionary of hints that can modify hook behavior.
+ /// A token to cancel the operation.
+ /// A completed representing the asynchronous operation.
+ public override ValueTask FinallyAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ var evaluationEvent = EvaluationEventBuilder.Build(context, details);
+
+ var tags = new ActivityTagsCollection();
+ foreach (var kvp in evaluationEvent.Attributes)
+ {
+ tags[kvp.Key] = kvp.Value;
+ }
+
+ Activity.Current?.AddEvent(new ActivityEvent(evaluationEvent.Name, tags: tags));
+
+ return base.FinallyAsync(context, details, hints, cancellationToken);
+ }
+}
diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj
index c47b109d..3d81a99e 100644
--- a/src/OpenFeature/OpenFeature.csproj
+++ b/src/OpenFeature/OpenFeature.csproj
@@ -9,6 +9,7 @@
+
diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs
new file mode 100644
index 00000000..54f6e19c
--- /dev/null
+++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs
@@ -0,0 +1,141 @@
+using OpenFeature.Hooks;
+using OpenFeature.Model;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+
+namespace OpenFeature.Tests.Hooks;
+
+[CollectionDefinition(nameof(MetricsHookTest), DisableParallelization = true)]
+public class MetricsHookTest : IDisposable
+{
+ private readonly List _exportedItems;
+ private readonly MeterProvider _meterProvider;
+
+ public MetricsHookTest()
+ {
+ // Arrange metrics collector
+ this._exportedItems = [];
+ this._meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddMeter("OpenFeature")
+ .ConfigureResource(r => r.AddService("open-feature"))
+ .AddInMemoryExporter(this._exportedItems,
+ option => option.PeriodicExportingMetricReaderOptions =
+ new PeriodicExportingMetricReaderOptions { ExportIntervalMilliseconds = 100 })
+ .Build();
+ }
+
+#pragma warning disable CA1816
+ public void Dispose()
+ {
+ this._meterProvider.Shutdown();
+ }
+#pragma warning restore CA1816
+
+ [Fact]
+ public async Task After_Test()
+ {
+ // Arrange
+ const string metricName = "feature_flag.evaluation_success_total";
+ var metricsHook = new MetricsHook();
+ var evaluationContext = EvaluationContext.Empty;
+ var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String,
+ new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext);
+
+ // Act
+ await metricsHook.AfterAsync(ctx,
+ new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"),
+ new Dictionary()).ConfigureAwait(true);
+ this._meterProvider.ForceFlush();
+
+ // Assert metrics
+ Assert.NotEmpty(this._exportedItems);
+
+ // check if the metric is present in the exported items
+ var metric = this._exportedItems.FirstOrDefault(m => m.Name == metricName);
+ Assert.NotNull(metric);
+
+ var noOtherMetric = this._exportedItems.All(m => m.Name == metricName);
+ Assert.True(noOtherMetric);
+ }
+
+ [Fact]
+ public async Task Error_Test()
+ {
+ // Arrange
+ const string metricName = "feature_flag.evaluation_error_total";
+ var metricsHook = new MetricsHook();
+ var evaluationContext = EvaluationContext.Empty;
+ var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String,
+ new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext);
+
+ // Act
+ await metricsHook.ErrorAsync(ctx, new Exception(), new Dictionary()).ConfigureAwait(true);
+ this._meterProvider.ForceFlush();
+
+ // Assert metrics
+ Assert.NotEmpty(this._exportedItems);
+
+ // check if the metric is present in the exported items
+ var metric = this._exportedItems.FirstOrDefault(m => m.Name == metricName);
+ Assert.NotNull(metric);
+
+ var noOtherMetric = this._exportedItems.All(m => m.Name == metricName);
+ Assert.True(noOtherMetric);
+ }
+
+ [Fact]
+ public async Task Finally_Test()
+ {
+ // Arrange
+ const string metricName = "feature_flag.evaluation_active_count";
+ var metricsHook = new MetricsHook();
+ var evaluationContext = EvaluationContext.Empty;
+ var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String,
+ new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext);
+ var evaluationDetails = new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default");
+
+ // Act
+ await metricsHook.FinallyAsync(ctx, evaluationDetails, new Dictionary()).ConfigureAwait(true);
+ this._meterProvider.ForceFlush();
+
+ // Assert metrics
+ Assert.NotEmpty(this._exportedItems);
+
+ // check if the metric feature_flag.evaluation_success_total is present in the exported items
+ var metric = this._exportedItems.FirstOrDefault(m => m.Name == metricName);
+ Assert.NotNull(metric);
+
+ var noOtherMetric = this._exportedItems.All(m => m.Name == metricName);
+ Assert.True(noOtherMetric);
+ }
+
+ [Fact]
+ public async Task Before_Test()
+ {
+ // Arrange
+ const string metricName1 = "feature_flag.evaluation_active_count";
+ const string metricName2 = "feature_flag.evaluation_requests_total";
+ var metricsHook = new MetricsHook();
+ var evaluationContext = EvaluationContext.Empty;
+ var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String,
+ new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext);
+
+ // Act
+ await metricsHook.BeforeAsync(ctx, new Dictionary()).ConfigureAwait(true);
+ this._meterProvider.ForceFlush();
+
+ // Assert metrics
+ Assert.NotEmpty(this._exportedItems);
+
+ // check if the metric is present in the exported items
+ var metric1 = this._exportedItems.FirstOrDefault(m => m.Name == metricName1);
+ Assert.NotNull(metric1);
+
+ var metric2 = this._exportedItems.FirstOrDefault(m => m.Name == metricName2);
+ Assert.NotNull(metric2);
+
+ var noOtherMetric = this._exportedItems.All(m => m.Name == metricName1 || m.Name == metricName2);
+ Assert.True(noOtherMetric);
+ }
+}
diff --git a/test/OpenFeature.Tests/Hooks/TraceEnricherHookTests.cs b/test/OpenFeature.Tests/Hooks/TraceEnricherHookTests.cs
new file mode 100644
index 00000000..f73d3620
--- /dev/null
+++ b/test/OpenFeature.Tests/Hooks/TraceEnricherHookTests.cs
@@ -0,0 +1,91 @@
+using System.Diagnostics;
+using OpenFeature.Hooks;
+using OpenFeature.Model;
+using OpenTelemetry;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+
+namespace OpenFeature.Tests.Hooks;
+
+[CollectionDefinition(nameof(TraceEnricherHookTests), DisableParallelization = true)]
+public class TraceEnricherHookTests : IDisposable
+{
+ private readonly List _exportedItems;
+ private readonly TracerProvider _tracerProvider;
+ private readonly Tracer _tracer;
+
+ public TraceEnricherHookTests()
+ {
+ // List that will be populated with the traces by InMemoryExporter
+ this._exportedItems = [];
+
+ // Create a new in-memory exporter
+ this._tracerProvider = Sdk.CreateTracerProviderBuilder()
+ .AddSource("my-tracer")
+ .ConfigureResource(r => r.AddService("in-memory-test"))
+ .AddInMemoryExporter(this._exportedItems)
+ .Build();
+
+ this._tracer = this._tracerProvider.GetTracer("my-tracer");
+ }
+
+#pragma warning disable CA1816
+ public void Dispose()
+ {
+ this._tracerProvider.Shutdown();
+ }
+#pragma warning restore CA1816
+
+ [Fact]
+ public async Task TestFinally()
+ {
+ // Arrange
+ var traceEnricherHook = new TraceEnricherHook();
+ var evaluationContext = EvaluationContext.Empty;
+ var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String,
+ new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext);
+
+ // Act
+ var span = this._tracer.StartActiveSpan("my-span");
+ await traceEnricherHook.FinallyAsync(ctx,
+ new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"),
+ new Dictionary()).ConfigureAwait(true);
+ span.End();
+
+ this._tracerProvider.ForceFlush();
+
+ // Assert
+ Assert.Single(this._exportedItems);
+ var rootSpan = this._exportedItems.First();
+
+ Assert.Single(rootSpan.Events);
+ ActivityEvent ev = rootSpan.Events.First();
+ Assert.Equal("feature_flag.evaluation", ev.Name);
+
+ Assert.Contains(new KeyValuePair("feature_flag.key", "my-flag"), ev.Tags);
+ Assert.Contains(new KeyValuePair("feature_flag.result.variant", "default"), ev.Tags);
+ Assert.Contains(new KeyValuePair("feature_flag.provider.name", "my-provider"), ev.Tags);
+ Assert.Contains(new KeyValuePair("feature_flag.result.reason", "static"), ev.Tags);
+ Assert.Contains(new KeyValuePair("feature_flag.result.value", "foo"), ev.Tags);
+ }
+
+ [Fact]
+ public async Task TestFinally_NoSpan()
+ {
+ // Arrange
+ var traceEnricherHook = new TraceEnricherHook();
+ var evaluationContext = EvaluationContext.Empty;
+ var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String,
+ new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext);
+
+ // Act
+ await traceEnricherHook.FinallyAsync(ctx,
+ new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"),
+ new Dictionary()).ConfigureAwait(true);
+
+ this._tracerProvider.ForceFlush();
+
+ // Assert
+ Assert.Empty(this._exportedItems);
+ }
+}
diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj
index a556655a..8abb4891 100644
--- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj
+++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj
@@ -19,6 +19,8 @@
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive