Skip to content
45 changes: 1 addition & 44 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,56 +373,13 @@ private async ValueTask<EvaluationEvent> EvaluateFeature<TContext>(string featur
Activity.Current != null &&
Activity.Current.IsAllDataRequested)
{
AddEvaluationActivityEvent(evaluationEvent);
FeatureEvaluationTelemetry.Publish(evaluationEvent, Logger);
}
}

return evaluationEvent;
}

private void AddEvaluationActivityEvent(EvaluationEvent evaluationEvent)
{
Debug.Assert(evaluationEvent != null);
Debug.Assert(evaluationEvent.FeatureDefinition != null);

var tags = new ActivityTagsCollection()
{
{ "FeatureName", evaluationEvent.FeatureDefinition.Name },
{ "Enabled", evaluationEvent.Enabled },
{ "VariantAssignmentReason", evaluationEvent.VariantAssignmentReason },
{ "Version", ActivitySource.Version }
};

if (!string.IsNullOrEmpty(evaluationEvent.TargetingContext?.UserId))
{
tags["TargetingId"] = evaluationEvent.TargetingContext.UserId;
}

if (!string.IsNullOrEmpty(evaluationEvent.Variant?.Name))
{
tags["Variant"] = evaluationEvent.Variant.Name;
}

if (evaluationEvent.FeatureDefinition.Telemetry.Metadata != null)
{
foreach (KeyValuePair<string, string> kvp in evaluationEvent.FeatureDefinition.Telemetry.Metadata)
{
if (tags.ContainsKey(kvp.Key))
{
Logger?.LogWarning($"{kvp.Key} from telemetry metadata will be ignored, as it would override an existing key.");

continue;
}

tags[kvp.Key] = kvp.Value;
}
}

var activityEvent = new ActivityEvent("FeatureFlag", DateTimeOffset.UtcNow, tags);

Activity.Current.AddEvent(activityEvent);
}

private async ValueTask<bool> IsEnabledAsync<TContext>(FeatureDefinition featureDefinition, TContext appContext, bool useAppContext, CancellationToken cancellationToken)
{
Debug.Assert(featureDefinition != null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Microsoft.FeatureManagement.Telemetry
{
internal static class FeatureEvaluationTelemetry
{
private static readonly string EvaluationEventVersion = "1.0.0";

/// <summary>
/// Handles an evaluation event by adding it as an activity event to the current Activity.
/// </summary>
/// <param name="evaluationEvent">The <see cref="EvaluationEvent"/> to publish as an <see cref="ActivityEvent"/></param>
/// <param name="logger">Optional logger to log warnings to</param>
public static void Publish(EvaluationEvent evaluationEvent, ILogger logger)
{
if (Activity.Current == null)
{
throw new InvalidOperationException("An Activity must be created before calling this method.");
}

if (evaluationEvent == null)
{
throw new ArgumentNullException(nameof(evaluationEvent));
}

if (evaluationEvent.FeatureDefinition == null)
{
throw new ArgumentNullException(nameof(evaluationEvent.FeatureDefinition));
}

var tags = new ActivityTagsCollection()
{
{ "FeatureName", evaluationEvent.FeatureDefinition.Name },
{ "Enabled", evaluationEvent.Enabled },
{ "VariantAssignmentReason", evaluationEvent.VariantAssignmentReason },
{ "Version", EvaluationEventVersion }
};

if (!string.IsNullOrEmpty(evaluationEvent.TargetingContext?.UserId))
{
tags["TargetingId"] = evaluationEvent.TargetingContext.UserId;
}

if (!string.IsNullOrEmpty(evaluationEvent.Variant?.Name))
{
tags["Variant"] = evaluationEvent.Variant.Name;
}

if (evaluationEvent.FeatureDefinition.Telemetry.Metadata != null)
{
foreach (KeyValuePair<string, string> kvp in evaluationEvent.FeatureDefinition.Telemetry.Metadata)
{
if (tags.ContainsKey(kvp.Key))
{
logger?.LogWarning($"{kvp.Key} from telemetry metadata will be ignored, as it would override an existing key.");

continue;
}

tags[kvp.Key] = kvp.Value;
}
}

// VariantAssignmentPercentage
if (evaluationEvent.VariantAssignmentReason == VariantAssignmentReason.DefaultWhenEnabled)
{
// If the variant was assigned due to DefaultWhenEnabled, the percentage reflects the unallocated percentiles
double allocatedPercentage = evaluationEvent.FeatureDefinition.Allocation?.Percentile?.Sum(p => p.To - p.From) ?? 0;

tags["VariantAssignmentPercentage"] = 100 - allocatedPercentage;
}
else if (evaluationEvent.VariantAssignmentReason == VariantAssignmentReason.Percentile)
{
// If the variant was assigned due to Percentile, the percentage is the sum of the allocated percentiles for the given variant
if (evaluationEvent.FeatureDefinition.Allocation?.Percentile != null)
{
tags["VariantAssignmentPercentage"] = evaluationEvent.FeatureDefinition.Allocation.Percentile
.Where(p => p.Variant == evaluationEvent.Variant?.Name)
.Sum(p => p.To - p.From);
}
}

// DefaultWhenEnabled
if (evaluationEvent.FeatureDefinition.Allocation?.DefaultWhenEnabled != null)
{
tags["DefaultWhenEnabled"] = evaluationEvent.FeatureDefinition.Allocation.DefaultWhenEnabled;
}

var activityEvent = new ActivityEvent("FeatureFlag", DateTimeOffset.UtcNow, tags);

Activity.Current.AddEvent(activityEvent);
}
}
}
32 changes: 32 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,9 @@ public async Task TelemetryPublishing()
string label = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Label").Value?.ToString();
string firstTag = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Tags.Tag1").Value?.ToString();

string variantAssignmentPercentage = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "VariantAssignmentPercentage").Value?.ToString();
string defaultWhenEnabled = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "DefaultWhenEnabled").Value?.ToString();

// Test telemetry cases
switch (featureName)
{
Expand Down Expand Up @@ -1783,6 +1786,8 @@ public async Task TelemetryPublishing()
Assert.Equal("True", enabled);
Assert.Equal("Medium", variantName);
Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled.ToString(), variantAssignmentReason);
Assert.Equal("100", variantAssignmentPercentage);
Assert.Equal("Medium", defaultWhenEnabled);
break;

case Features.VariantFeatureDefaultDisabled:
Expand All @@ -1791,6 +1796,8 @@ public async Task TelemetryPublishing()
Assert.Equal("False", enabled);
Assert.Equal("Small", variantName);
Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled.ToString(), variantAssignmentReason);
Assert.Null(variantAssignmentPercentage);
Assert.Null(defaultWhenEnabled);
break;

case Features.VariantFeaturePercentileOn:
Expand All @@ -1813,41 +1820,62 @@ public async Task TelemetryPublishing()
currentTest = 0;
Assert.Null(variantName);
Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled.ToString(), variantAssignmentReason);
Assert.Null(variantAssignmentPercentage);
Assert.Null(defaultWhenEnabled);
break;

case Features.VariantFeatureUser:
Assert.Equal(8, currentTest);
currentTest = 0;
Assert.Equal("Small", variantName);
Assert.Equal(VariantAssignmentReason.User.ToString(), variantAssignmentReason);
Assert.Null(variantAssignmentPercentage);
Assert.Null(defaultWhenEnabled);
break;

case Features.VariantFeatureGroup:
Assert.Equal(9, currentTest);
currentTest = 0;
Assert.Equal("Small", variantName);
Assert.Equal(VariantAssignmentReason.Group.ToString(), variantAssignmentReason);
Assert.Null(variantAssignmentPercentage);
Assert.Null(defaultWhenEnabled);
break;

case Features.VariantFeatureNoVariants:
Assert.Equal(10, currentTest);
currentTest = 0;
Assert.Null(variantName);
Assert.Equal(VariantAssignmentReason.None.ToString(), variantAssignmentReason);
Assert.Null(variantAssignmentPercentage);
Assert.Null(defaultWhenEnabled);
break;

case Features.VariantFeatureNoAllocation:
Assert.Equal(11, currentTest);
currentTest = 0;
Assert.Null(variantName);
Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled.ToString(), variantAssignmentReason);
Assert.Equal("100", variantAssignmentPercentage);
Assert.Null(defaultWhenEnabled);
break;

case Features.VariantFeatureAlwaysOffNoAllocation:
Assert.Equal(12, currentTest);
currentTest = 0;
Assert.Null(variantName);
Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled.ToString(), variantAssignmentReason);
Assert.Null(variantAssignmentPercentage);
Assert.Null(defaultWhenEnabled);
break;

case Features.VariantFeatureIncorrectDefaultWhenEnabled:
Assert.Equal(13, currentTest);
currentTest = 0;
Assert.Null(variantName);
Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled.ToString(), variantAssignmentReason);
Assert.Equal("100", variantAssignmentPercentage);
Assert.Equal("Foo", defaultWhenEnabled);
break;

default:
Expand Down Expand Up @@ -1912,6 +1940,10 @@ public async Task TelemetryPublishing()
await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOffNoAllocation, cancellationToken);
Assert.Equal(0, currentTest);

currentTest = 13;
await featureManager.GetVariantAsync(Features.VariantFeatureIncorrectDefaultWhenEnabled, cancellationToken);
Assert.Equal(0, currentTest);

// Test a feature with telemetry disabled- should throw if the listener hits it
bool result = await featureManager.IsEnabledAsync(Features.OnTestFeature, cancellationToken);

Expand Down
1 change: 1 addition & 0 deletions tests/Tests.FeatureManagement/Features.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ static class Features
public const string VariantFeatureGroup = "VariantFeatureGroup";
public const string VariantFeatureNoVariants = "VariantFeatureNoVariants";
public const string VariantFeatureNoAllocation = "VariantFeatureNoAllocation";
public const string VariantFeatureIncorrectDefaultWhenEnabled = "VariantFeatureIncorrectDefaultWhenEnabled";
public const string VariantFeatureAlwaysOffNoAllocation = "VariantFeatureAlwaysOffNoAllocation";
public const string VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride";
public const string VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo";
Expand Down
22 changes: 19 additions & 3 deletions tests/Tests.FeatureManagement/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@
"to": 50
}
],
"seed": 1234
"seed": "1234"
},
"telemetry": {
"enabled": true
Expand All @@ -231,7 +231,7 @@
"to": 50
}
],
"seed": 12345
"seed": "12345"
},
"telemetry": {
"enabled": true
Expand All @@ -253,7 +253,7 @@
"to": 100
}
],
"seed": 12345
"seed": "12345"
},
"telemetry": {
"enabled": true
Expand Down Expand Up @@ -383,6 +383,22 @@
"enabled": true
}
},
{
"id": "VariantFeatureIncorrectDefaultWhenEnabled",
"enabled": true,
"variants": [
{
"name": "Small",
"configuration_value": "300px"
}
],
"allocation": {
"default_when_enabled": "Foo"
},
"telemetry": {
"enabled": true
}
},
{
"id": "VariantFeatureAlwaysOffNoAllocation",
"enabled": false,
Expand Down