Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<MajorVersion>4</MajorVersion>
<MinorVersion>0</MinorVersion>
<PatchVersion>0</PatchVersion>
<PreviewVersion>-preview5</PreviewVersion>
</PropertyGroup>

<Import Project="..\..\build\Versioning.props" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<MajorVersion>4</MajorVersion>
<MinorVersion>0</MinorVersion>
<PatchVersion>0</PatchVersion>
<PreviewVersion>-preview5</PreviewVersion>
</PropertyGroup>

<Import Project="..\..\build\Versioning.props" />
Expand Down
46 changes: 1 addition & 45 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,57 +368,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);

// FeatureEvaluation event schema: https://github.com/microsoft/FeatureManagement/blob/main/Schema/FeatureEvaluationEvent/FeatureEvaluationEvent.v1.0.0.schema.json
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("{key} from telemetry metadata will be ignored, as it would override an existing key.", kvp.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
Expand Up @@ -7,6 +7,7 @@
<MajorVersion>4</MajorVersion>
<MinorVersion>0</MinorVersion>
<PatchVersion>0</PatchVersion>
<PreviewVersion>-preview5</PreviewVersion>
</PropertyGroup>

<Import Project="..\..\build\Versioning.props" />
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 @@ -1706,6 +1706,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 @@ -1733,6 +1736,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 @@ -1741,6 +1746,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 @@ -1763,41 +1770,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 @@ -1862,6 +1890,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