diff --git a/Directory.Packages.props b/Directory.Packages.props
index ab69e552..91b5679a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -25,6 +25,8 @@
+
+
@@ -37,4 +39,4 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 0aaeb399..f9f16ed6 100644
--- a/README.md
+++ b/README.md
@@ -413,6 +413,121 @@ services.AddOpenFeature()
});
```
+### Trace Hook
+
+For this hook to function correctly a global `TracerProvider` must be set, an example of how to do this can be found below.
+
+The `open telemetry hook` taps into the after and error methods of the hook lifecycle to write `events` and `attributes` to an existing `span`.
+For this, an active span must be set in the `Tracer`, otherwise the hook will no-op.
+
+### Example
+
+The following example demonstrates the use of the `OpenTelemetry hook` 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 Otel Hook to the OpenFeature instance
+ OpenFeature.Api.Instance.AddHooks(new TracingHook());
+
+ 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, variant |
+| feature_flag.evaluation_error_total | Flag evaluation errors | Counter | key, provider name |
+| feature_flag.evaluation_active_count | Active flag evaluations counter | Counter | key |
+
+Consider the following code example for usage.
+
+### Example
+
+The following example demonstrates the use of the `OpenTelemetry hook` 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 Otel Hook 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/src/OpenFeature/Hooks/MetricsConstants.cs b/src/OpenFeature/Hooks/MetricsConstants.cs
new file mode 100644
index 00000000..7b020fb3
--- /dev/null
+++ b/src/OpenFeature/Hooks/MetricsConstants.cs
@@ -0,0 +1,20 @@
+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 KeyAttr = "key";
+ internal const string ProviderNameAttr = "provider_name";
+ internal const string VariantAttr = "variant";
+ internal const string ReasonAttr = "reason";
+ 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..b6d088a5
--- /dev/null
+++ b/src/OpenFeature/Hooks/MetricsHook.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenFeature.Model;
+
+namespace OpenFeature.Hooks;
+
+///
+/// Represents a hook for capturing metrics related to flag evaluations.
+/// The meter instrumentation name is "OpenFeature".
+///
+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 readonly UpDownCounter _evaluationActiveUpDownCounter;
+ private readonly Counter _evaluationRequestCounter;
+ private readonly Counter _evaluationSuccessCounter;
+ private readonly Counter _evaluationErrorCounter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MetricsHook()
+ {
+ var meter = new Meter(InstrumentationName, InstrumentationVersion);
+
+ 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
+ {
+ { MetricsConstants.KeyAttr, context.FlagKey },
+ { MetricsConstants.ProviderNameAttr, 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
+ {
+ { MetricsConstants.KeyAttr, context.FlagKey },
+ { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name },
+ { MetricsConstants.VariantAttr, details.Variant ?? details.Value?.ToString() },
+ { MetricsConstants.ReasonAttr, details.Reason ?? "UNKNOWN" }
+ };
+
+ 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
+ {
+ { MetricsConstants.KeyAttr, context.FlagKey },
+ { MetricsConstants.ProviderNameAttr, 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
+ {
+ { MetricsConstants.KeyAttr, context.FlagKey },
+ { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }
+ };
+
+ this._evaluationActiveUpDownCounter.Add(-1, tagList);
+
+ return base.FinallyAsync(context, evaluationDetails, hints, cancellationToken);
+ }
+}
diff --git a/src/OpenFeature/Hooks/TracingConstants.cs b/src/OpenFeature/Hooks/TracingConstants.cs
new file mode 100644
index 00000000..27be6d7c
--- /dev/null
+++ b/src/OpenFeature/Hooks/TracingConstants.cs
@@ -0,0 +1,9 @@
+namespace OpenFeature.Hooks;
+
+internal static class TracingConstants
+{
+ internal const string AttributeExceptionEventName = "exception";
+ internal const string AttributeExceptionType = "exception.type";
+ internal const string AttributeExceptionMessage = "exception.message";
+ internal const string AttributeExceptionStacktrace = "exception.stacktrace";
+}
diff --git a/src/OpenFeature/Hooks/TracingHook.cs b/src/OpenFeature/Hooks/TracingHook.cs
new file mode 100644
index 00000000..b3afdd46
--- /dev/null
+++ b/src/OpenFeature/Hooks/TracingHook.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenFeature.Model;
+
+namespace OpenFeature.Hooks;
+
+///
+/// Stub.
+///
+public class TracingHook : Hook
+{
+ ///
+ public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details,
+ IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+ Activity.Current?
+ .SetTag("feature_flag.key", details.FlagKey)
+ .SetTag("feature_flag.variant", details.Variant)
+ .SetTag("feature_flag.provider_name", context.ProviderMetadata.Name)
+ .AddEvent(new ActivityEvent("feature_flag", tags: new ActivityTagsCollection
+ {
+ ["feature_flag.key"] = details.FlagKey,
+ ["feature_flag.variant"] = details.Variant,
+ ["feature_flag.provider_name"] = context.ProviderMetadata.Name
+ }));
+
+ return default;
+ }
+
+ ///
+ public override ValueTask ErrorAsync(HookContext context, System.Exception error,
+ IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default)
+ {
+#if NET9_0_OR_GREATER
+ // For dotnet9 we should use the new API https://learn.microsoft.com/en-gb/dotnet/api/system.diagnostics.activity.addexception?view=net-9.0
+ Activity.Current?.AddException(error);
+#else
+ var tagsCollection = new ActivityTagsCollection
+ {
+ { TracingConstants.AttributeExceptionType, error.GetType().FullName },
+ { TracingConstants.AttributeExceptionStacktrace, error.ToString() },
+ };
+ if (!string.IsNullOrWhiteSpace(error.Message))
+ {
+ tagsCollection.Add(TracingConstants.AttributeExceptionMessage, error.Message);
+ }
+
+ Activity.Current?.AddEvent(new ActivityEvent(TracingConstants.AttributeExceptionEventName, default, tagsCollection));
+#endif
+ return default;
+ }
+}
diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs
new file mode 100644
index 00000000..5192df37
--- /dev/null
+++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using OpenFeature.Hooks;
+using OpenFeature.Model;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using Xunit;
+
+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/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs
new file mode 100644
index 00000000..0c236f40
--- /dev/null
+++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using OpenFeature.Hooks;
+using OpenFeature.Model;
+using OpenTelemetry;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+using Xunit;
+
+namespace OpenFeature.Tests.Hooks;
+
+[CollectionDefinition(nameof(TracingHookTest), DisableParallelization = true)]
+public class TracingHookTest : IDisposable
+{
+ private readonly List _exportedItems;
+ private readonly TracerProvider _tracerProvider;
+ private readonly Tracer _tracer;
+
+ public TracingHookTest()
+ {
+ // 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 TestAfter()
+ {
+ // Arrange
+ var tracingHook = new TracingHook();
+ 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 tracingHook.AfterAsync(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", ev.Name);
+
+ Assert.Contains(new KeyValuePair("feature_flag.key", "my-flag"), ev.Tags);
+ Assert.Contains(new KeyValuePair("feature_flag.variant", "default"), ev.Tags);
+ Assert.Contains(new KeyValuePair("feature_flag.provider_name", "my-provider"), ev.Tags);
+ }
+
+ [Fact]
+ public async Task TestAfter_NoSpan()
+ {
+ // Arrange
+ var tracingHook = new TracingHook();
+ 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 tracingHook.AfterAsync(ctx,
+ new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"),
+ new Dictionary()).ConfigureAwait(true);
+
+ this._tracerProvider.ForceFlush();
+
+ // Assert
+ Assert.Empty(this._exportedItems);
+ }
+
+ [Fact]
+ public async Task TestError()
+ {
+ // Arrange
+ var tracingHook = new TracingHook();
+ 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 tracingHook.ErrorAsync(ctx, new Exception("unexpected error"),
+ new Dictionary()).ConfigureAwait(true);
+ span.End();
+
+ this._tracerProvider.ForceFlush();
+
+ // Assert
+ Assert.Single(this._exportedItems);
+ var rootSpan = this._exportedItems.First();
+
+ Assert.Single(rootSpan.Events);
+ var ev = rootSpan.Events.First();
+
+ Assert.Equal("exception", ev.Name);
+
+ Assert.Contains(new KeyValuePair("exception.message", "unexpected error"), ev.Tags);
+ }
+
+ [Fact]
+ public async Task TestError_NoSpan()
+ {
+ // Arrange
+ var tracingHook = new TracingHook();
+ 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 tracingHook.ErrorAsync(ctx, new Exception("unexpected error"),
+ 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 4df0c681..b0eb6bd1 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