Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
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())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add AddSource("OpenFeature") to collect OpenFeature activity?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added trace source in 3b8eac5

.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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the SetTag/SetStatus, think we could add tags in one place and add tags only when IsAllDataRequested

https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activity.isalldatarequested?view=net-10.0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6e943fa - this was a great spot! It helps avoid allocations in high performance scenarios

I wonder if we should add feature_flag.provider.name in all instances or only when IsAllDataRequested is true


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we like to keep these tagName/activityName to be constants in a separated place?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6e943fa - moved the tag names (apart from error.type) to the OpenFeatureActivitySource class

}

// 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"]);
}

[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