Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3b609d9
Add OpenTelemetry log exporting and ensure traces reliably sample
kylejuliandev May 6, 2026
703798f
Add code to start activity and initial unit tests
kylejuliandev May 6, 2026
73ee185
Add additional unit tests and ensure tags are added
kylejuliandev May 7, 2026
24ea6b6
Fix build issue
kylejuliandev May 7, 2026
3dce87e
Address gemini code review comments
kylejuliandev May 8, 2026
a6b8ec3
Add tests to improve code coverage of OpenFeatureActivitySource
kylejuliandev May 8, 2026
7e99375
Set activity kind to internal
kylejuliandev May 8, 2026
3b8eac5
Ensure major.minor.patch version is included in trace
kylejuliandev May 14, 2026
6e943fa
Move tag keys to string constants in separate class
kylejuliandev May 14, 2026
6d64f4f
Remove duplicate activity disposal
kylejuliandev May 14, 2026
21c9a1c
Add internal extension method for adding tags to activities
kylejuliandev May 20, 2026
23d0839
Merge branch 'main' into kylej/improve-tracing
kylejuliandev May 20, 2026
06fc54b
Remove null check on Activity and create constant for error.type
kylejuliandev May 21, 2026
6f51561
docs: Add notice to README on how to get new traces from OpenFeature
kylejuliandev Jun 18, 2026
870b7c0
Merge branch 'main' into kylej/improve-tracing
kylejuliandev Jun 18, 2026
1ba2ef9
Address coderabbit review comments
kylejuliandev Jun 18, 2026
4fff690
Merge branch 'main' into kylej/improve-tracing
kylejuliandev Jun 18, 2026
ca5b1e6
Ensure `feature_flag.result.reason` is added in exception branches
kylejuliandev Jun 18, 2026
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
13 changes: 13 additions & 0 deletions samples/AspNetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,23 @@
builder.Services.AddProblemDetails();

// Configure OpenTelemetry
builder.Logging.AddOpenTelemetry(options =>
{
options

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (windows-latest, x64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (windows-latest, x64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (ubuntu-latest, x64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (ubuntu-latest, x64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (ubuntu-24.04-arm, arm64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (ubuntu-24.04-arm, arm64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (macos-latest, arm64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (macos-latest, arm64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (macos-15-intel, x64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (macos-15-intel, x64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (windows-11-arm, arm64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'

Check failure on line 30 in samples/AspNetCore/Program.cs

View workflow job for this annotation

GitHub Actions / AOT Test (windows-11-arm, arm64)

'OpenTelemetryLoggerOptions' does not contain a definition for 'AddOtlpExporter' and the best extension method overload 'OtlpMetricExporterExtensions.AddOtlpExporter(MeterProviderBuilder)' requires a receiver of type 'OpenTelemetry.Metrics.MeterProviderBuilder'
.SetResourceBuilder(
ResourceBuilder.CreateDefault()
.AddService("openfeature-aspnetcore-sample"))
.AddOtlpExporter();

options.IncludeScopes = true;
options.IncludeFormattedMessage = true;
});

builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("openfeature-aspnetcore-sample"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.SetSampler(new AlwaysOnSampler())
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
Expand Down
48 changes: 48 additions & 0 deletions src/OpenFeature/OpenFeatureActivitySource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Diagnostics;
using System.Reflection;
using OpenFeature.Constant;

namespace OpenFeature;

static class OpenFeatureActivitySource
{
static readonly ActivitySource Source = new("OpenFeature", GetLibraryVersion());

internal static Activity? StartActivity(string name)
=> Source.StartActivity(name, ActivityKind.Client);

// Mapped to standard `error.types` https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-events/#evaluation-event
internal static string GetFlagEvaluationErrorDescription(this ErrorType errorType) =>
errorType switch
{
ErrorType.ProviderNotReady => "provider_not_ready",
ErrorType.FlagNotFound => "flag_not_found",
ErrorType.ParseError => "parse_error",
ErrorType.TypeMismatch => "type_mismatch",
ErrorType.General => "general",
ErrorType.InvalidContext => "invalid_context",
ErrorType.TargetingKeyMissing => "targeting_key_missing",
ErrorType.ProviderFatal => "provider_fatal",
_ => "_OTHER"
};

// Mapped to standard `feature_flag.result.reason` https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-events/#evaluation-event
internal static string? GetFlagEvaluationReasonDescription(string? reason) =>
reason switch
{
Reason.TargetingMatch => "targeting_match",
Reason.Split => "split",
Reason.Disabled => "disabled",
Reason.Default => "default",
Reason.Static => "static",
Reason.Cached => "cached",
Reason.Unknown => "unknown",
Reason.Error => "error",
_ => reason
Comment thread
kylejuliandev marked this conversation as resolved.
};

static string GetLibraryVersion()
=> typeof(OpenFeatureActivitySource).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion ?? "UNKNOWN";
Comment thread
askpt marked this conversation as resolved.
Outdated
}
27 changes: 27 additions & 0 deletions src/OpenFeature/OpenFeatureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlagAsync<T>(
var resolveValueDelegate = providerInfo.Item1;
var provider = providerInfo.Item2;

var activity = OpenFeatureActivitySource.StartActivity("feature_flag.evaluation");
Comment thread
askpt marked this conversation as resolved.
Outdated
Comment thread
askpt marked this conversation as resolved.
Outdated
activity?.SetTag("feature_flag.key", flagKey);
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated

var providerMetadata = provider.GetMetadata();
if (providerMetadata != null)
{
activity?.SetTag("feature_flag.provider.name", providerMetadata.Name);
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated
}

// New up an evaluation context if one was not provided.
context ??= EvaluationContext.Empty;

Expand Down Expand Up @@ -261,8 +270,13 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlagAsync<T>(
.ConfigureAwait(false))
.ToFlagEvaluationDetails();

activity?.SetTag("feature_flag.result.reason", OpenFeatureActivitySource.GetFlagEvaluationReasonDescription(evaluation.Reason));
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated

if (evaluation.ErrorType == ErrorType.None)
{
activity?.SetTag("feature_flag.result.value", evaluation.Value);
activity?.SetTag("feature_flag.result.variant", evaluation.Variant);
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated

await hookRunner.TriggerAfterHooksAsync(
evaluation,
options?.HookHints,
Expand All @@ -271,6 +285,9 @@ await hookRunner.TriggerAfterHooksAsync(
}
else
{
activity?.AddTag("error.type", OpenFeatureActivitySource.GetFlagEvaluationErrorDescription(evaluation.ErrorType));
activity?.SetTag("feature_flag.error.message", evaluation.ErrorMessage);
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated

var exception = new FeatureProviderException(evaluation.ErrorType, evaluation.ErrorMessage);
this.FlagEvaluationErrorWithDescription(flagKey, evaluation.ErrorType.GetDescription(), exception);
await hookRunner.TriggerErrorHooksAsync(exception, options?.HookHints, cancellationToken)
Expand All @@ -282,6 +299,10 @@ await hookRunner.TriggerErrorHooksAsync(exception, options?.HookHints, cancellat
this.FlagEvaluationErrorWithDescription(flagKey, ex.ErrorType.GetDescription(), ex);
evaluation = new FlagEvaluationDetails<T>(flagKey, defaultValue, ex.ErrorType, Reason.Error,
string.Empty, ex.Message);

activity?.AddTag("error.type", OpenFeatureActivitySource.GetFlagEvaluationErrorDescription(evaluation.ErrorType));
activity?.SetTag("feature_flag.error.message", evaluation.ErrorMessage);
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated

await hookRunner.TriggerErrorHooksAsync(ex, options?.HookHints, cancellationToken)
.ConfigureAwait(false);
}
Expand All @@ -290,6 +311,10 @@ await hookRunner.TriggerErrorHooksAsync(ex, options?.HookHints, cancellationToke
var errorCode = ex is InvalidCastException ? ErrorType.TypeMismatch : ErrorType.General;
evaluation = new FlagEvaluationDetails<T>(flagKey, defaultValue, errorCode, Reason.Error, string.Empty,
ex.Message);

activity?.AddTag("error.type", OpenFeatureActivitySource.GetFlagEvaluationErrorDescription(evaluation.ErrorType));
activity?.SetTag("feature_flag.error.message", evaluation.ErrorMessage);
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated

await hookRunner.TriggerErrorHooksAsync(ex, options?.HookHints, cancellationToken)
.ConfigureAwait(false);
}
Expand All @@ -302,6 +327,8 @@ await hookRunner.TriggerFinallyHooksAsync(evaluation, options?.HookHints, cancel
.ConfigureAwait(false);
}

activity?.Dispose();
Comment thread
kylejuliandev marked this conversation as resolved.
Outdated

return evaluation;
}

Expand Down
219 changes: 219 additions & 0 deletions test/OpenFeature.Tests/OpenFeatureClientTracingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
using System.Diagnostics;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using OpenFeature.Constant;
using OpenFeature.Error;
using OpenFeature.Model;
using OpenFeature.Providers.Memory;

namespace OpenFeature.Tests;

public class OpenFeatureClientTracingTests : IAsyncLifetime
{
private readonly Api _api;

private readonly List<Activity> _exportedActivities;
private readonly ActivityListener _activityListener;

public OpenFeatureClientTracingTests()
{
this._api = Api.Instance;
this._exportedActivities = [];
this._activityListener = new ActivityListener()
{
ShouldListenTo = source => source.Name == "OpenFeature",
Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStopped = activity => this._exportedActivities.Add(activity)
};

ActivitySource.AddActivityListener(this._activityListener);
}

public Task InitializeAsync()
{
var flags = new Dictionary<string, Flag>
{
["bool-flag"] = new Flag<bool>(new Dictionary<string, bool> { { "on", true } }, "on"),
["string-flag"] = new Flag<string>(new Dictionary<string, string> { { "on", "hello" } }, "on"),
["int-flag"] = new Flag<int>(new Dictionary<string, int> { { "on", 42 } }, "on"),
["double-flag"] = new Flag<double>(new Dictionary<string, double> { { "on", 3.14 } }, "on"),
["object-flag"] = new Flag<Value>(new Dictionary<string, Value> { { "on", new Value(Structure.Builder().Set("value1", true).Build()) } }, "on")
};
var provider = new InMemoryProvider(flags);

return this._api.SetProviderAsync(provider);
}

public static IEnumerable<object[]> ResolveValue()
{
yield return new object[]
{
new Func<FeatureClient, Task<FlagEvaluationDetails<bool>>>((r) => r.GetBooleanDetailsAsync("bool-flag", false))
};
yield return new object[]
{
new Func<FeatureClient, Task<FlagEvaluationDetails<string>>>((r) => r.GetStringDetailsAsync("string-flag", "def"))
};
yield return new object[]
{
new Func<FeatureClient, Task<FlagEvaluationDetails<int>>>((r) => r.GetIntegerDetailsAsync("int-flag", 3))
};
yield return new object[]
{
new Func<FeatureClient, Task<FlagEvaluationDetails<double>>>((r) => r.GetDoubleDetailsAsync("double-flag", 3.5))
};
yield return new object[]
{
new Func<FeatureClient, Task<FlagEvaluationDetails<Value>>>((r) => r.GetObjectDetailsAsync("object-flag", new Value(Structure.Builder().Set("value1", true).Build())))
};
}

[Theory]
[MemberData(nameof(ResolveValue))]
public async Task GetValueAsync_ShouldCreateSpan<T>(Func<FeatureClient, Task<FlagEvaluationDetails<T>>> act)
{
// Arrange
var client = this._api.GetClient("TestClient");

// Act
var result = await act(client);

// Assert
Assert.Single(this._exportedActivities);

var trace = this._exportedActivities[0];
var tags = trace.TagObjects.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Assert.Contains("feature_flag.key", tags);
Assert.Equal(result.FlagKey, tags["feature_flag.key"]);

Assert.Contains("feature_flag.provider.name", tags);
Assert.Equal("InMemory", tags["feature_flag.provider.name"]);

Assert.Contains("feature_flag.result.reason", tags);
Assert.Equal("static", tags["feature_flag.result.reason"]);

Assert.Contains("feature_flag.result.value", tags);
Assert.Equal(result.Value, tags["feature_flag.result.value"]);

Assert.Contains("feature_flag.result.variant", tags);
Assert.Equal("on", tags["feature_flag.result.variant"]);
}

[Theory]
[MemberData(nameof(ResolveValue))]
public async Task GetValueAsync_WhenProviderErrors_ShouldCreateSpanWithErrorTags<T>(Func<FeatureClient, Task<FlagEvaluationDetails<T>>> act)
{
// Arrange
var mockProvider = Substitute.For<FeatureProvider>();
mockProvider.GetMetadata().Returns(new Metadata("TestProvider"));
mockProvider.ResolveBooleanValueAsync("bool-flag", Arg.Any<bool>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new ResolutionDetails<bool>("bool-flag", true, ErrorType.ProviderFatal, errorMessage: "Error!")));
mockProvider.ResolveStringValueAsync("string-flag", Arg.Any<string>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new ResolutionDetails<string>("string-flag", "world", ErrorType.ProviderFatal, errorMessage: "Error!")));
mockProvider.ResolveIntegerValueAsync("int-flag", Arg.Any<int>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new ResolutionDetails<int>("int-flag", 42, ErrorType.ProviderFatal, errorMessage: "Error!")));
mockProvider.ResolveDoubleValueAsync("double-flag", Arg.Any<double>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new ResolutionDetails<double>("double-flag", 1.0f, ErrorType.ProviderFatal, errorMessage: "Error!")));
mockProvider.ResolveStructureValueAsync("object-flag", Arg.Any<Value>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new ResolutionDetails<Value>("object-flag", new Value(Structure.Builder().Build()), ErrorType.ProviderFatal, errorMessage: "Error!")));

await this._api.SetProviderAsync("domain", mockProvider);

var client = this._api.GetClient("domain");

// Act
await act(client);

// Assert
Assert.Single(this._exportedActivities);

var trace = this._exportedActivities[0];
var tags = trace.TagObjects.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Assert.Contains("error.type", tags);
Assert.Equal("provider_fatal", tags["error.type"]);

Assert.Contains("feature_flag.error.message", tags);
Assert.Equal("Error!", tags["feature_flag.error.message"]);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

[Theory]
[MemberData(nameof(ResolveValue))]
public async Task GetValueAsync_WhenProviderThrowsException_ShouldCreateSpanWithErrorTags<T>(Func<FeatureClient, Task<FlagEvaluationDetails<T>>> act)
{
// Arrange
var mockProvider = Substitute.For<FeatureProvider>();
mockProvider.GetMetadata().Returns(new Metadata("TestProvider"));
mockProvider.ResolveBooleanValueAsync("bool-flag", Arg.Any<bool>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new FeatureProviderException(ErrorType.TargetingKeyMissing, "Error!"));
mockProvider.ResolveStringValueAsync("string-flag", Arg.Any<string>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new FeatureProviderException(ErrorType.TargetingKeyMissing, "Error!"));
mockProvider.ResolveIntegerValueAsync("int-flag", Arg.Any<int>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new FeatureProviderException(ErrorType.TargetingKeyMissing, "Error!"));
mockProvider.ResolveDoubleValueAsync("double-flag", Arg.Any<double>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new FeatureProviderException(ErrorType.TargetingKeyMissing, "Error!"));
mockProvider.ResolveStructureValueAsync("object-flag", Arg.Any<Value>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new FeatureProviderException(ErrorType.TargetingKeyMissing, "Error!"));

await this._api.SetProviderAsync("domain", mockProvider);

var client = this._api.GetClient("domain");

// Act
await act(client);

// Assert
Assert.Single(this._exportedActivities);

var trace = this._exportedActivities[0];
var tags = trace.TagObjects.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Assert.Contains("error.type", tags);
Assert.Equal("targeting_key_missing", tags["error.type"]);

Assert.Contains("feature_flag.error.message", tags);
Assert.Equal("Error!", tags["feature_flag.error.message"]);
}

[Theory]
[MemberData(nameof(ResolveValue))]
public async Task GetValueAsync_WhenProviderThrowsGeneralException_ShouldCreateSpanWithErrorTags<T>(Func<FeatureClient, Task<FlagEvaluationDetails<T>>> act)
{
// Arrange
var mockProvider = Substitute.For<FeatureProvider>();
mockProvider.GetMetadata().Returns(new Metadata("TestProvider"));
mockProvider.ResolveBooleanValueAsync("bool-flag", Arg.Any<bool>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new Exception("Error!"));
mockProvider.ResolveStringValueAsync("string-flag", Arg.Any<string>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new Exception("Error!"));
mockProvider.ResolveIntegerValueAsync("int-flag", Arg.Any<int>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new Exception("Error!"));
mockProvider.ResolveDoubleValueAsync("double-flag", Arg.Any<double>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new Exception("Error!"));
mockProvider.ResolveStructureValueAsync("object-flag", Arg.Any<Value>(), Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
.ThrowsAsync(new Exception("Error!"));

await this._api.SetProviderAsync("domain", mockProvider);

var client = this._api.GetClient("domain");

// Act
await act(client);

// Assert
Assert.Single(this._exportedActivities);

var trace = this._exportedActivities[0];
var tags = trace.TagObjects.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Assert.Contains("error.type", tags);
Assert.Equal("general", tags["error.type"]);

Assert.Contains("feature_flag.error.message", tags);
Assert.Equal("Error!", tags["feature_flag.error.message"]);
}

public Task DisposeAsync()
{
this._activityListener.Dispose();

return this._api.ShutdownAsync();
}
}
Loading