diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs index 02aed91a..77605462 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. // using Microsoft.ApplicationInsights; +using System.Diagnostics; namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights { @@ -30,15 +31,17 @@ public ValueTask PublishEvent(EvaluationEvent evaluationEvent, CancellationToken FeatureDefinition featureDefinition = evaluationEvent.FeatureDefinition; - Dictionary properties = new Dictionary() + var properties = new Dictionary() { { "FeatureName", featureDefinition.Name }, - { "IsEnabled", evaluationEvent.IsEnabled.ToString() } + { "Enabled", evaluationEvent.Enabled.ToString() } }; - if (evaluationEvent.Variant != null) + if (evaluationEvent.VariantAssignmentReason != VariantAssignmentReason.None) { - properties["Variant"] = evaluationEvent.Variant.Name; + properties["Variant"] = evaluationEvent.Variant?.Name; + + properties["VariantAssignmentReason"] = ToString(evaluationEvent.VariantAssignmentReason); } if (featureDefinition.TelemetryMetadata != null) @@ -66,5 +69,26 @@ private void ValidateEvent(EvaluationEvent evaluationEvent) throw new ArgumentNullException(nameof(evaluationEvent.FeatureDefinition)); } } + + private static string ToString(VariantAssignmentReason reason) + { + Debug.Assert(reason != VariantAssignmentReason.None); + + const string DefaultWhenDisabled = "DefaultWhenDisabled"; + const string DefaultWhenEnabled = "DefaultWhenEnabled"; + const string User = "User"; + const string Group = "Group"; + const string Percentile = "Percentile"; + + return reason switch + { + VariantAssignmentReason.DefaultWhenDisabled => DefaultWhenDisabled, + VariantAssignmentReason.DefaultWhenEnabled => DefaultWhenEnabled, + VariantAssignmentReason.User => User, + VariantAssignmentReason.Group => Group, + VariantAssignmentReason.Percentile => Percentile, + _ => throw new ArgumentException("Invalid assignment reason.", nameof(reason)) + }; + } } } \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj index ea1077cb..43a107f9 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj @@ -3,7 +3,6 @@ net6.0 enable - enable diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 8c0853a8..ff0671f3 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -144,9 +144,11 @@ public TargetingEvaluationOptions AssignerOptions /// /// The name of the feature to check. /// True if the feature is enabled, otherwise false. - public Task IsEnabledAsync(string feature) + public async Task IsEnabledAsync(string feature) { - return IsEnabledWithVariantsAsync(feature, appContext: null, useAppContext: false, CancellationToken.None).AsTask(); + EvaluationEvent evaluationEvent = await EvaluateFeature(feature, context: null, useContext: false, CancellationToken.None); + + return evaluationEvent.Enabled; } /// @@ -155,9 +157,11 @@ public Task IsEnabledAsync(string feature) /// The name of the feature to check. /// A context providing information that can be used to evaluate whether a feature should be on or off. /// True if the feature is enabled, otherwise false. - public Task IsEnabledAsync(string feature, TContext appContext) + public async Task IsEnabledAsync(string feature, TContext appContext) { - return IsEnabledWithVariantsAsync(feature, appContext, useAppContext: true, CancellationToken.None).AsTask(); + EvaluationEvent evaluationEvent = await EvaluateFeature(feature, context: appContext, useContext: true, CancellationToken.None); + + return evaluationEvent.Enabled; } /// @@ -166,9 +170,11 @@ public Task IsEnabledAsync(string feature, TContext appContext) /// The name of the feature to check. /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - public ValueTask IsEnabledAsync(string feature, CancellationToken cancellationToken) + public async ValueTask IsEnabledAsync(string feature, CancellationToken cancellationToken) { - return IsEnabledWithVariantsAsync(feature, appContext: null, useAppContext: false, cancellationToken); + EvaluationEvent evaluationEvent = await EvaluateFeature(feature, context: null, useContext: false, cancellationToken); + + return evaluationEvent.Enabled; } /// @@ -178,9 +184,11 @@ public ValueTask IsEnabledAsync(string feature, CancellationToken cancella /// A context providing information that can be used to evaluate whether a feature should be on or off. /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - public ValueTask IsEnabledAsync(string feature, TContext appContext, CancellationToken cancellationToken) + public async ValueTask IsEnabledAsync(string feature, TContext appContext, CancellationToken cancellationToken) { - return IsEnabledWithVariantsAsync(feature, appContext, useAppContext: true, cancellationToken); + EvaluationEvent evaluationEvent = await EvaluateFeature(feature, context: appContext, useContext: true, cancellationToken); + + return evaluationEvent.Enabled; } /// @@ -212,14 +220,16 @@ public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellati /// The name of the feature to evaluate. /// The cancellation token to cancel the operation. /// A variant assigned to the user based on the feature's configured allocation. - public ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) + public async ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(feature)) { throw new ArgumentNullException(nameof(feature)); } - return GetVariantAsync(feature, context: null, useContext: false, cancellationToken); + EvaluationEvent evaluationEvent = await EvaluateFeature(feature, context: null, useContext: false, cancellationToken); + + return evaluationEvent.Variant; } /// @@ -229,7 +239,7 @@ public ValueTask GetVariantAsync(string feature, CancellationToken canc /// An instance of used to evaluate which variant the user will be assigned. /// The cancellation token to cancel the operation. /// A variant assigned to the user based on the feature's configured allocation. - public ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken) + public async ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(feature)) { @@ -241,77 +251,110 @@ public ValueTask GetVariantAsync(string feature, TargetingContext conte throw new ArgumentNullException(nameof(context)); } - return GetVariantAsync(feature, context, useContext: true, cancellationToken); + EvaluationEvent evaluationEvent = await EvaluateFeature(feature, context, useContext: true, cancellationToken); + + return evaluationEvent.Variant; } - private async ValueTask IsEnabledWithVariantsAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) + private async Task EvaluateFeature(string feature, TContext context, bool useContext, CancellationToken cancellationToken) { - bool isFeatureEnabled = false; - - FeatureDefinition featureDefinition = await GetFeatureDefinition(feature).ConfigureAwait(false); - - VariantDefinition variantDefinition = null; + var evaluationEvent = new EvaluationEvent + { + FeatureDefinition = await GetFeatureDefinition(feature).ConfigureAwait(false) + }; - if (featureDefinition != null) + if (evaluationEvent.FeatureDefinition != null) { - isFeatureEnabled = await IsEnabledAsync(featureDefinition, appContext, useAppContext, cancellationToken).ConfigureAwait(false); + // + // Determine IsEnabled + evaluationEvent.Enabled = await IsEnabledAsync(evaluationEvent.FeatureDefinition, context, useContext, cancellationToken).ConfigureAwait(false); - if (featureDefinition.Variants != null && featureDefinition.Variants.Any() && featureDefinition.Allocation != null) + // + // Determine Variant + VariantDefinition variantDefinition = null; + + if (evaluationEvent.FeatureDefinition.Variants != null && + evaluationEvent.FeatureDefinition.Variants.Any()) { - if (!isFeatureEnabled) + if (evaluationEvent.FeatureDefinition.Allocation == null) { - variantDefinition = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == featureDefinition.Allocation.DefaultWhenDisabled); + evaluationEvent.VariantAssignmentReason = evaluationEvent.Enabled ? + VariantAssignmentReason.DefaultWhenEnabled : + VariantAssignmentReason.DefaultWhenDisabled; + } + else if (!evaluationEvent.Enabled) + { + if (evaluationEvent.FeatureDefinition.Allocation.DefaultWhenDisabled != null) + { + variantDefinition = evaluationEvent.FeatureDefinition + .Variants + .FirstOrDefault(variant => + variant.Name == evaluationEvent.FeatureDefinition.Allocation.DefaultWhenDisabled); + } + + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.DefaultWhenDisabled; } else { TargetingContext targetingContext; - if (useAppContext) + if (useContext) { - targetingContext = appContext as TargetingContext; + targetingContext = context as TargetingContext; } else { targetingContext = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false); } - variantDefinition = await GetAssignedVariantAsync( - featureDefinition, - targetingContext, - cancellationToken) - .ConfigureAwait(false); - } + if (targetingContext != null && evaluationEvent.FeatureDefinition.Allocation != null) + { + variantDefinition = await AssignVariantAsync(evaluationEvent, targetingContext, cancellationToken).ConfigureAwait(false); + } - if (variantDefinition != null && featureDefinition.Status != FeatureStatus.Disabled) + if (evaluationEvent.VariantAssignmentReason == VariantAssignmentReason.None) + { + if (evaluationEvent.FeatureDefinition.Allocation.DefaultWhenEnabled != null) + { + variantDefinition = evaluationEvent.FeatureDefinition + .Variants + .FirstOrDefault(variant => + variant.Name == evaluationEvent.FeatureDefinition.Allocation.DefaultWhenEnabled); + } + + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.DefaultWhenEnabled; + } + } + + evaluationEvent.Variant = variantDefinition != null ? GetVariantFromVariantDefinition(variantDefinition) : null; + + // + // Override IsEnabled if variant has an override + if (variantDefinition != null && evaluationEvent.FeatureDefinition.Status != FeatureStatus.Disabled) { if (variantDefinition.StatusOverride == StatusOverride.Enabled) { - isFeatureEnabled = true; + evaluationEvent.Enabled = true; } else if (variantDefinition.StatusOverride == StatusOverride.Disabled) { - isFeatureEnabled = false; + evaluationEvent.Enabled = false; } } } - } - foreach (ISessionManager sessionManager in _sessionManagers) - { - await sessionManager.SetAsync(feature, isFeatureEnabled).ConfigureAwait(false); - } + foreach (ISessionManager sessionManager in _sessionManagers) + { + await sessionManager.SetAsync(evaluationEvent.FeatureDefinition.Name, evaluationEvent.Enabled).ConfigureAwait(false); + } - if (featureDefinition.TelemetryEnabled) - { - PublishTelemetry(new EvaluationEvent + if (evaluationEvent.FeatureDefinition.TelemetryEnabled) { - FeatureDefinition = featureDefinition, - IsEnabled = isFeatureEnabled, - Variant = variantDefinition != null ? GetVariantFromVariantDefinition(variantDefinition) : null - }, cancellationToken); + PublishTelemetry(evaluationEvent, cancellationToken); + } } - return isFeatureEnabled; + return evaluationEvent; } private async Task IsEnabledAsync(FeatureDefinition featureDefinition, TContext appContext, bool useAppContext, CancellationToken cancellationToken) @@ -455,48 +498,6 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) return enabled; } - private async ValueTask GetVariantAsync(string feature, TargetingContext context, bool useContext, CancellationToken cancellationToken) - { - FeatureDefinition featureDefinition = await GetFeatureDefinition(feature).ConfigureAwait(false); - - if (featureDefinition == null || featureDefinition.Allocation == null || (!featureDefinition.Variants?.Any() ?? false)) - { - return null; - } - - VariantDefinition variantDefinition = null; - - bool isFeatureEnabled = await IsEnabledAsync(featureDefinition, context, useContext, cancellationToken).ConfigureAwait(false); - - if (!isFeatureEnabled) - { - variantDefinition = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == featureDefinition.Allocation.DefaultWhenDisabled); - } - else - { - if (!useContext) - { - context = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false); - } - - variantDefinition = await GetAssignedVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); - } - - Variant variant = variantDefinition != null ? GetVariantFromVariantDefinition(variantDefinition) : null; - - if (featureDefinition.TelemetryEnabled) - { - PublishTelemetry(new EvaluationEvent - { - FeatureDefinition = featureDefinition, - IsEnabled = isFeatureEnabled, - Variant = variant - }, cancellationToken); - } - - return variant; - } - private async ValueTask GetFeatureDefinition(string feature) { FeatureDefinition featureDefinition = await _featureDefinitionProvider @@ -541,97 +542,95 @@ private async ValueTask ResolveTargetingContextAsync(Cancellat return context; } - private async ValueTask GetAssignedVariantAsync(FeatureDefinition featureDefinition, TargetingContext context, CancellationToken cancellationToken) + private ValueTask AssignVariantAsync(EvaluationEvent evaluationEvent, TargetingContext targetingContext, CancellationToken cancellationToken) { - VariantDefinition variantDefinition = null; + Debug.Assert(evaluationEvent != null); - if (context != null) - { - variantDefinition = await AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); - } + Debug.Assert(targetingContext != null); - if (variantDefinition == null) - { - variantDefinition = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == featureDefinition.Allocation.DefaultWhenEnabled); - } - - return variantDefinition; - } + Debug.Assert(evaluationEvent.FeatureDefinition.Allocation != null); - private ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) - { VariantDefinition variant = null; - if (featureDefinition.Allocation.User != null) + if (evaluationEvent.FeatureDefinition.Allocation.User != null) { - foreach (UserAllocation user in featureDefinition.Allocation.User) + foreach (UserAllocation user in evaluationEvent.FeatureDefinition.Allocation.User) { if (TargetingEvaluator.IsTargeted(targetingContext.UserId, user.Users, _assignerOptions.IgnoreCase)) { if (string.IsNullOrEmpty(user.Variant)) { - Logger?.LogWarning($"Missing variant name for user allocation in feature {featureDefinition.Name}"); + Logger?.LogWarning($"Missing variant name for user allocation in feature {evaluationEvent.FeatureDefinition.Name}"); return new ValueTask((VariantDefinition)null); } - Debug.Assert(featureDefinition.Variants != null); + Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null); + + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.User; return new ValueTask( - featureDefinition + evaluationEvent.FeatureDefinition .Variants - .FirstOrDefault((variant) => variant.Name == user.Variant)); + .FirstOrDefault(variant => + variant.Name == user.Variant)); } } } - if (featureDefinition.Allocation.Group != null) + if (evaluationEvent.FeatureDefinition.Allocation.Group != null) { - foreach (GroupAllocation group in featureDefinition.Allocation.Group) + foreach (GroupAllocation group in evaluationEvent.FeatureDefinition.Allocation.Group) { if (TargetingEvaluator.IsTargeted(targetingContext.Groups, group.Groups, _assignerOptions.IgnoreCase)) { if (string.IsNullOrEmpty(group.Variant)) { - Logger?.LogWarning($"Missing variant name for group allocation in feature {featureDefinition.Name}"); + Logger?.LogWarning($"Missing variant name for group allocation in feature {evaluationEvent.FeatureDefinition.Name}"); return new ValueTask((VariantDefinition)null); } - Debug.Assert(featureDefinition.Variants != null); + Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null); + + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.Group; return new ValueTask( - featureDefinition + evaluationEvent.FeatureDefinition .Variants - .FirstOrDefault((variant) => variant.Name == group.Variant)); + .FirstOrDefault(variant => + variant.Name == group.Variant)); } } } - if (featureDefinition.Allocation.Percentile != null) + if (evaluationEvent.FeatureDefinition.Allocation.Percentile != null) { - foreach (PercentileAllocation percentile in featureDefinition.Allocation.Percentile) + foreach (PercentileAllocation percentile in evaluationEvent.FeatureDefinition.Allocation.Percentile) { if (TargetingEvaluator.IsTargeted( targetingContext, percentile.From, percentile.To, _assignerOptions.IgnoreCase, - featureDefinition.Allocation.Seed ?? $"allocation\n{featureDefinition.Name}")) + evaluationEvent.FeatureDefinition.Allocation.Seed ?? $"allocation\n{evaluationEvent.FeatureDefinition.Name}")) { if (string.IsNullOrEmpty(percentile.Variant)) { - Logger?.LogWarning($"Missing variant name for percentile allocation in feature {featureDefinition.Name}"); + Logger?.LogWarning($"Missing variant name for percentile allocation in feature {evaluationEvent.FeatureDefinition.Name}"); return new ValueTask((VariantDefinition)null); } - Debug.Assert(featureDefinition.Variants != null); + Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null); + + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.Percentile; return new ValueTask( - featureDefinition + evaluationEvent.FeatureDefinition .Variants - .FirstOrDefault((variant) => variant.Name == percentile.Variant)); + .FirstOrDefault(variant => + variant.Name == percentile.Variant)); } } } diff --git a/src/Microsoft.FeatureManagement/Telemetry/EvaluationEvent.cs b/src/Microsoft.FeatureManagement/Telemetry/EvaluationEvent.cs index a425c290..aca0cdfc 100644 --- a/src/Microsoft.FeatureManagement/Telemetry/EvaluationEvent.cs +++ b/src/Microsoft.FeatureManagement/Telemetry/EvaluationEvent.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using Microsoft.FeatureManagement.FeatureFilters; - namespace Microsoft.FeatureManagement.Telemetry { /// @@ -18,11 +16,16 @@ public class EvaluationEvent /// /// The enabled state of the feature after evaluation. /// - public bool IsEnabled { get; set; } + public bool Enabled { get; set; } /// /// The variant given after evaluation. /// public Variant Variant { get; set; } + + /// + /// The reason why the variant was assigned. + /// + public VariantAssignmentReason VariantAssignmentReason { get; set; } } } diff --git a/src/Microsoft.FeatureManagement/Telemetry/VariantAssignmentReason.cs b/src/Microsoft.FeatureManagement/Telemetry/VariantAssignmentReason.cs new file mode 100644 index 00000000..a4db27d4 --- /dev/null +++ b/src/Microsoft.FeatureManagement/Telemetry/VariantAssignmentReason.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement.Telemetry +{ + /// + /// The reason the variant was assigned during the evaluation of a feature. + /// + public enum VariantAssignmentReason + { + /// + /// Variant allocation did not happend. No variant is assigned. + /// + None, + + /// + /// The default variant is assigned when a feature flag is disabled. + /// + DefaultWhenDisabled, + + /// + /// The default variant is assigned because of no applicable user/group/percentile allocation when a feature flag is enabled. + /// + DefaultWhenEnabled, + + /// + /// The variant is assigned because of the user allocation when a feature flag is enabled. + /// + User, + + /// + /// The variant is assigned because of the group allocation when a feature flag is enabled. + /// + Group, + + /// + /// The variant is assigned because of the percentile allocation when a feature flag is enabled. + /// + Percentile + } +} diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index da94f35b..0a7aa357 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; +using Microsoft.FeatureManagement.Telemetry; using Microsoft.FeatureManagement.Tests; using System; using System.Collections.Generic; @@ -355,7 +356,7 @@ public async Task Percentage() } } - Assert.True(enabledCount >= 0 && enabledCount < 10); + Assert.True(enabledCount >= 0 && enabledCount <= 10); } [Fact] @@ -1010,73 +1011,114 @@ public async Task TelemetryPublishing() var services = new ServiceCollection(); - services + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + services.AddSingleton(targetingContextAccessor) .AddSingleton(config) .AddFeatureManagement() - .AddTelemetryPublisher() - .AddFeatureFilter(); + .AddTelemetryPublisher(); ServiceProvider serviceProvider = services.BuildServiceProvider(); FeatureManager featureManager = (FeatureManager) serviceProvider.GetRequiredService(); TestTelemetryPublisher testPublisher = (TestTelemetryPublisher) featureManager.TelemetryPublishers.First(); + CancellationToken cancellationToken = CancellationToken.None; // Test a feature with telemetry disabled - bool result = await featureManager.IsEnabledAsync(Features.OnTestFeature, CancellationToken.None); + bool result = await featureManager.IsEnabledAsync(Features.OnTestFeature, cancellationToken); Assert.True(result); Assert.Null(testPublisher.evaluationEventCache); // Test telemetry cases - const string onFeature = "AlwaysOnTestFeature"; - - result = await featureManager.IsEnabledAsync(onFeature, CancellationToken.None); + result = await featureManager.IsEnabledAsync(Features.AlwaysOnTestFeature, cancellationToken); Assert.True(result); - Assert.Equal(onFeature, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(result, testPublisher.evaluationEventCache.IsEnabled); + Assert.Equal(Features.AlwaysOnTestFeature, testPublisher.evaluationEventCache.FeatureDefinition.Name); + Assert.Equal(result, testPublisher.evaluationEventCache.Enabled); Assert.Equal("EtagValue", testPublisher.evaluationEventCache.FeatureDefinition.TelemetryMetadata["Etag"]); Assert.Equal("LabelValue", testPublisher.evaluationEventCache.FeatureDefinition.TelemetryMetadata["Label"]); Assert.Equal("Tag1Value", testPublisher.evaluationEventCache.FeatureDefinition.TelemetryMetadata["Tags.Tag1"]); + Assert.Null(testPublisher.evaluationEventCache.Variant); + Assert.Equal(VariantAssignmentReason.None, testPublisher.evaluationEventCache.VariantAssignmentReason); - const string offFeature = "OffTimeTestFeature"; - - result = await featureManager.IsEnabledAsync(offFeature, CancellationToken.None); + result = await featureManager.IsEnabledAsync(Features.OffTimeTestFeature, cancellationToken); Assert.False(result); - Assert.Equal(offFeature, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(result, testPublisher.evaluationEventCache.IsEnabled); + Assert.Equal(Features.OffTimeTestFeature, testPublisher.evaluationEventCache.FeatureDefinition.Name); + Assert.Equal(result, testPublisher.evaluationEventCache.Enabled); + Assert.Equal(VariantAssignmentReason.None, testPublisher.evaluationEventCache.VariantAssignmentReason); // Test variant cases - const string variantDefaultEnabledFeature = "VariantFeatureDefaultEnabled"; - - result = await featureManager.IsEnabledAsync(variantDefaultEnabledFeature, CancellationToken.None); + result = await featureManager.IsEnabledAsync(Features.VariantFeatureDefaultEnabled, cancellationToken); Assert.True(result); - Assert.Equal(variantDefaultEnabledFeature, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(result, testPublisher.evaluationEventCache.IsEnabled); + Assert.Equal(Features.VariantFeatureDefaultEnabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); + Assert.Equal(result, testPublisher.evaluationEventCache.Enabled); Assert.Equal("Medium", testPublisher.evaluationEventCache.Variant.Name); - Variant variantResult = await featureManager.GetVariantAsync(variantDefaultEnabledFeature, CancellationToken.None); + Variant variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureDefaultEnabled, cancellationToken); - Assert.True(testPublisher.evaluationEventCache.IsEnabled); - Assert.Equal(variantDefaultEnabledFeature, testPublisher.evaluationEventCache.FeatureDefinition.Name); + Assert.True(testPublisher.evaluationEventCache.Enabled); + Assert.Equal(Features.VariantFeatureDefaultEnabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); Assert.Equal(variantResult.Name, testPublisher.evaluationEventCache.Variant.Name); + Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled, testPublisher.evaluationEventCache.VariantAssignmentReason); - string variantFeatureStatusDisabled = "VariantFeatureStatusDisabled"; - - result = await featureManager.IsEnabledAsync(variantFeatureStatusDisabled, CancellationToken.None); + result = await featureManager.IsEnabledAsync(Features.VariantFeatureStatusDisabled, cancellationToken); Assert.False(result); - Assert.Equal(variantFeatureStatusDisabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(result, testPublisher.evaluationEventCache.IsEnabled); + Assert.Equal(Features.VariantFeatureStatusDisabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); + Assert.Equal(result, testPublisher.evaluationEventCache.Enabled); Assert.Equal("Small", testPublisher.evaluationEventCache.Variant.Name); + Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason); - variantResult = await featureManager.GetVariantAsync(variantFeatureStatusDisabled, CancellationToken.None); + variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureStatusDisabled, cancellationToken); - Assert.False(testPublisher.evaluationEventCache.IsEnabled); - Assert.Equal(variantFeatureStatusDisabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); + Assert.False(testPublisher.evaluationEventCache.Enabled); + Assert.Equal(Features.VariantFeatureStatusDisabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); Assert.Equal(variantResult.Name, testPublisher.evaluationEventCache.Variant.Name); + Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason); + + targetingContextAccessor.Current = new TargetingContext + { + UserId = "Marsha", + Groups = new List { "Group1" } + }; + + variantResult = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOn, cancellationToken); + Assert.Equal("Big", variantResult.Name); + Assert.Equal("Big", testPublisher.evaluationEventCache.Variant.Name); + Assert.Equal(VariantAssignmentReason.Percentile, testPublisher.evaluationEventCache.VariantAssignmentReason); + + variantResult = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOff, cancellationToken); + Assert.Null(variantResult); + Assert.Null(testPublisher.evaluationEventCache.Variant); + Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled, testPublisher.evaluationEventCache.VariantAssignmentReason); + + variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOff, cancellationToken); + Assert.Null(variantResult); + Assert.Null(testPublisher.evaluationEventCache.Variant); + Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason); + + variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureUser, cancellationToken); + Assert.Equal("Small", variantResult.Name); + Assert.Equal("Small", testPublisher.evaluationEventCache.Variant.Name); + Assert.Equal(VariantAssignmentReason.User, testPublisher.evaluationEventCache.VariantAssignmentReason); + + variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureGroup, cancellationToken); + Assert.Equal("Small", variantResult.Name); + Assert.Equal("Small", testPublisher.evaluationEventCache.Variant.Name); + Assert.Equal(VariantAssignmentReason.Group, testPublisher.evaluationEventCache.VariantAssignmentReason); + + variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureNoAllocation, cancellationToken); + Assert.Null(variantResult); + Assert.Null(testPublisher.evaluationEventCache.Variant); + Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled, testPublisher.evaluationEventCache.VariantAssignmentReason); + + variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOffNoAllocation, cancellationToken); + Assert.Null(variantResult); + Assert.Null(testPublisher.evaluationEventCache.Variant); + Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason); + } [Fact] @@ -1088,17 +1130,14 @@ public async Task TelemetryPublishingNullPublisher() services .AddSingleton(config) - .AddFeatureManagement() - .AddFeatureFilter(); + .AddFeatureManagement(); ServiceProvider serviceProvider = services.BuildServiceProvider(); FeatureManager featureManager = (FeatureManager)serviceProvider.GetRequiredService(); // Test telemetry enabled feature with no telemetry publisher - string onFeature = "AlwaysOnTestFeature"; - - bool result = await featureManager.IsEnabledAsync(onFeature, CancellationToken.None); + bool result = await featureManager.IsEnabledAsync(Features.AlwaysOnTestFeature, CancellationToken.None); Assert.True(result); } @@ -1127,44 +1166,44 @@ public async Task UsesVariants() }; // Test StatusOverride and Percentile with Seed - Variant variant = await featureManager.GetVariantAsync("VariantFeaturePercentileOn", cancellationToken); + Variant variant = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOn, cancellationToken); Assert.Equal("Big", variant.Name); Assert.Equal("green", variant.Configuration["Color"]); - Assert.False(await featureManager.IsEnabledAsync("VariantFeaturePercentileOn", cancellationToken)); + Assert.False(await featureManager.IsEnabledAsync(Features.VariantFeaturePercentileOn, cancellationToken)); - variant = await featureManager.GetVariantAsync("VariantFeaturePercentileOff", cancellationToken); + variant = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOff, cancellationToken); Assert.Null(variant); - Assert.True(await featureManager.IsEnabledAsync("VariantFeaturePercentileOff", cancellationToken)); + Assert.True(await featureManager.IsEnabledAsync(Features.VariantFeaturePercentileOff, cancellationToken)); // Test Status = Disabled - variant = await featureManager.GetVariantAsync("VariantFeatureStatusDisabled", cancellationToken); + variant = await featureManager.GetVariantAsync(Features.VariantFeatureStatusDisabled, cancellationToken); Assert.Equal("Small", variant.Name); Assert.Equal("300px", variant.Configuration.Value); - Assert.False(await featureManager.IsEnabledAsync("VariantFeatureStatusDisabled", cancellationToken)); + Assert.False(await featureManager.IsEnabledAsync(Features.VariantFeatureStatusDisabled, cancellationToken)); // Test DefaultWhenEnabled and ConfigurationValue with inline IConfigurationSection - variant = await featureManager.GetVariantAsync("VariantFeatureDefaultEnabled", cancellationToken); + variant = await featureManager.GetVariantAsync(Features.VariantFeatureDefaultEnabled, cancellationToken); Assert.Equal("Medium", variant.Name); Assert.Equal("450px", variant.Configuration["Size"]); - Assert.True(await featureManager.IsEnabledAsync("VariantFeatureDefaultEnabled", cancellationToken)); + Assert.True(await featureManager.IsEnabledAsync(Features.VariantFeatureDefaultEnabled, cancellationToken)); // Test User allocation - variant = await featureManager.GetVariantAsync("VariantFeatureUser", cancellationToken); + variant = await featureManager.GetVariantAsync(Features.VariantFeatureUser, cancellationToken); Assert.Equal("Small", variant.Name); Assert.Equal("300px", variant.Configuration.Value); - Assert.True(await featureManager.IsEnabledAsync("VariantFeatureUser", cancellationToken)); + Assert.True(await featureManager.IsEnabledAsync(Features.VariantFeatureUser, cancellationToken)); // Test Group allocation - variant = await featureManager.GetVariantAsync("VariantFeatureGroup", cancellationToken); + variant = await featureManager.GetVariantAsync(Features.VariantFeatureGroup, cancellationToken); Assert.Equal("Small", variant.Name); Assert.Equal("300px", variant.Configuration.Value); - Assert.True(await featureManager.IsEnabledAsync("VariantFeatureGroup", cancellationToken)); + Assert.True(await featureManager.IsEnabledAsync(Features.VariantFeatureGroup, cancellationToken)); } [Fact] @@ -1190,24 +1229,24 @@ public async Task VariantsInvalidScenarios() CancellationToken cancellationToken = CancellationToken.None; // Verify null variant returned if no variants are specified - Variant variant = await featureManager.GetVariantAsync("VariantFeatureNoVariants", cancellationToken); + Variant variant = await featureManager.GetVariantAsync(Features.VariantFeatureNoVariants, cancellationToken); Assert.Null(variant); // Verify null variant returned if no allocation is specified - variant = await featureManager.GetVariantAsync("VariantFeatureNoAllocation", cancellationToken); + variant = await featureManager.GetVariantAsync(Features.VariantFeatureNoAllocation, cancellationToken); Assert.Null(variant); // Verify that ConfigurationValue has priority over ConfigurationReference - variant = await featureManager.GetVariantAsync("VariantFeatureBothConfigurations", cancellationToken); + variant = await featureManager.GetVariantAsync(Features.VariantFeatureBothConfigurations, cancellationToken); Assert.Equal("600px", variant.Configuration.Value); // Verify that an exception is thrown for invalid StatusOverride value FeatureManagementException e = await Assert.ThrowsAsync(async () => { - variant = await featureManager.GetVariantAsync("VariantFeatureInvalidStatusOverride", cancellationToken); + variant = await featureManager.GetVariantAsync(Features.VariantFeatureInvalidStatusOverride, cancellationToken); }); Assert.Equal(FeatureManagementError.InvalidConfigurationSetting, e.Error); @@ -1216,7 +1255,7 @@ public async Task VariantsInvalidScenarios() // Verify that an exception is thrown for invalid doubles From and To in the Percentile section e = await Assert.ThrowsAsync(async () => { - variant = await featureManager.GetVariantAsync("VariantFeatureInvalidFromTo", cancellationToken); + variant = await featureManager.GetVariantAsync(Features.VariantFeatureInvalidFromTo, cancellationToken); }); Assert.Equal(FeatureManagementError.InvalidConfigurationSetting, e.Error); diff --git a/tests/Tests.FeatureManagement/Features.cs b/tests/Tests.FeatureManagement/Features.cs index b52ec008..d3d2b81f 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -9,11 +9,26 @@ static class Features public const string TargetingTestFeatureWithExclusion = "TargetingTestFeatureWithExclusion"; public const string OnTestFeature = "OnTestFeature"; public const string OffTestFeature = "OffTestFeature"; + public const string AlwaysOnTestFeature = "AlwaysOnTestFeature"; + public const string OffTimeTestFeature = "OffTimeTestFeature"; public const string ConditionalFeature = "ConditionalFeature"; public const string ConditionalFeature2 = "ConditionalFeature2"; public const string ContextualFeature = "ContextualFeature"; public const string AnyFilterFeature = "AnyFilterFeature"; public const string AllFilterFeature = "AllFilterFeature"; public const string FeatureUsesFiltersWithDuplicatedAlias = "FeatureUsesFiltersWithDuplicatedAlias"; + public const string VariantFeatureDefaultEnabled = "VariantFeatureDefaultEnabled"; + public const string VariantFeatureStatusDisabled = "VariantFeatureStatusDisabled"; + public const string VariantFeaturePercentileOn = "VariantFeaturePercentileOn"; + public const string VariantFeaturePercentileOff = "VariantFeaturePercentileOff"; + public const string VariantFeatureAlwaysOff = "VariantFeatureAlwaysOff"; + public const string VariantFeatureUser = "VariantFeatureUser"; + public const string VariantFeatureGroup = "VariantFeatureGroup"; + public const string VariantFeatureNoVariants = "VariantFeatureNoVariants"; + public const string VariantFeatureNoAllocation = "VariantFeatureNoAllocation"; + public const string VariantFeatureAlwaysOffNoAllocation = "VariantFeatureAlwaysOffNoAllocation"; + public const string VariantFeatureBothConfigurations = "VariantFeatureBothConfigurations"; + public const string VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride"; + public const string VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo"; } } diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index dff27db3..c7eed721 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -17,425 +17,460 @@ } }, - "FeatureManagement": { - "OnTestFeature": true, - "OffTestFeature": false, - "AlwaysOnTestFeature": { - "TelemetryEnabled": true, - "EnabledFor": [ - { - "Name": "AlwaysOn" - } - ], - "TelemetryMetadata": { - "Tags.Tag1": "Tag1Value", - "Tags.Tag2": "Tag2Value", - "Etag": "EtagValue", - "Label": "LabelValue" - } - }, - "OffTimeTestFeature": { - "TelemetryEnabled": true, - "EnabledFor": [ - { - "Name": "TimeWindow", - "Parameters": { - "End": "1970-01-01T00:00:00Z" - } - } - ] - }, - "FeatureUsesFiltersWithDuplicatedAlias": { - "RequirementType": "all", - "EnabledFor": [ - { - "Name": "DuplicatedFilterName" + "FeatureManagement": { + "OnTestFeature": true, + "OffTestFeature": false, + "AlwaysOnTestFeature": { + "TelemetryEnabled": true, + "EnabledFor": [ + { + "Name": "AlwaysOn" + } + ], + "TelemetryMetadata": { + "Tags.Tag1": "Tag1Value", + "Tags.Tag2": "Tag2Value", + "Etag": "EtagValue", + "Label": "LabelValue" + } }, - { - "Name": "Percentage", - "Parameters": { - "Value": 100 - } - } - ] - }, - "TargetingTestFeature": { - "EnabledFor": [ - { - "Name": "Targeting", - "Parameters": { - "Audience": { - "Users": [ - "Jeff", - "Alicia" - ], - "Groups": [ - { - "Name": "Ring0", - "RolloutPercentage": 100 + "OffTimeTestFeature": { + "TelemetryEnabled": true, + "EnabledFor": [ + { + "Name": "TimeWindow", + "Parameters": { + "End": "1970-01-01T00:00:00Z" + } + } + ] + }, + "FeatureUsesFiltersWithDuplicatedAlias": { + "RequirementType": "all", + "EnabledFor": [ + { + "Name": "DuplicatedFilterName" }, { - "Name": "Ring1", - "RolloutPercentage": 50 + "Name": "Percentage", + "Parameters": { + "Value": 100 + } } - ], - "DefaultRolloutPercentage": 20 - } - } - } - ] - }, - "TargetingTestFeatureWithExclusion": { - "EnabledFor": [ - { - "Name": "Targeting", - "Parameters": { - "Audience": { - "Users": [ - "Jeff", - "Alicia" - ], - "Groups": [ - { - "Name": "Ring0", - "RolloutPercentage": 100 + ] + }, + "TargetingTestFeature": { + "EnabledFor": [ + { + "Name": "Targeting", + "Parameters": { + "Audience": { + "Users": [ + "Jeff", + "Alicia" + ], + "Groups": [ + { + "Name": "Ring0", + "RolloutPercentage": 100 + }, + { + "Name": "Ring1", + "RolloutPercentage": 50 + } + ], + "DefaultRolloutPercentage": 20 + } + } + } + ] + }, + "TargetingTestFeatureWithExclusion": { + "EnabledFor": [ + { + "Name": "Targeting", + "Parameters": { + "Audience": { + "Users": [ + "Jeff", + "Alicia" + ], + "Groups": [ + { + "Name": "Ring0", + "RolloutPercentage": 100 + }, + { + "Name": "Ring1", + "RolloutPercentage": 50 + } + ], + "DefaultRolloutPercentage": 20, + "Exclusion": { + "Users": [ + "Jeff" + ], + "Groups": [ + "Ring0", + "Ring2" + ] + } + } + } + } + ] + }, + "CustomFilterFeature": { + "EnabledFor": [ + { + "Name": "CustomTargetingFilter", + "Parameters": { + "Audience": { + "Users": [ + "Jeff" + ] + } + } + } + ] + }, + "ConditionalFeature": { + "EnabledFor": [ + { + "Name": "Test", + "Parameters": { + "P1": "V1" + } + } + ] + }, + "ConditionalFeature2": { + "EnabledFor": [ + { + "Name": "Test" + } + ] + }, + "ContextualFeature": { + "EnabledFor": [ + { + "Name": "ContextualTest", + "Parameters": { + "AllowedAccounts": [ + "abc" + ] + } + } + ] + }, + "AnyFilterFeature": { + "RequirementType": "Any", + "EnabledFor": [ + { + "Name": "Test", + "Parameters": { + "Id": "1" + } }, { - "Name": "Ring1", - "RolloutPercentage": 50 + "Name": "Test", + "Parameters": { + "Id": "2" + } + } + ] + }, + "AllFilterFeature": { + "RequirementType": "all", + "EnabledFor": [ + { + "Name": "Test", + "Parameters": { + "Id": "1" + } + }, + { + "Name": "Test", + "Parameters": { + "Id": "2" + } + } + ] + + }, + "VariantFeaturePercentileOn": { + "TelemetryEnabled": true, + "Allocation": { + "Percentile": [ + { + "Variant": "Big", + "From": 0, + "To": 50 + } + ], + "Seed": 1234 + }, + "Variants": [ + { + "Name": "Big", + "ConfigurationReference": "ShoppingCart:Big", + "StatusOverride": "Disabled" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeaturePercentileOff": { + "TelemetryEnabled": true, + "Allocation": { + "Percentile": [ + { + "Variant": "Big", + "From": 0, + "To": 50 + } + ], + "Seed": 12345 + }, + "Variants": [ + { + "Name": "Big", + "ConfigurationReference": "ShoppingCart:Big" + } + ], + "EnabledFor": [ + { + "Name": "On" } - ], - "DefaultRolloutPercentage": 20, - "Exclusion": { - "Users": [ - "Jeff" + ] + }, + "VariantFeatureAlwaysOff": { + "TelemetryEnabled": true, + "Allocation": { + "Percentile": [ + { + "Variant": "Big", + "From": 0, + "To": 100 + } ], - "Groups": [ - "Ring0", - "Ring2" + "Seed": 12345 + }, + "Variants": [ + { + "Name": "Big", + "ConfigurationReference": "ShoppingCart:Big" + } + ], + "EnabledFor": [] + }, + "VariantFeatureStatusDisabled": { + "Status": "Disabled", + "TelemetryEnabled": true, + "Allocation": { + "DefaultWhenDisabled": "Small" + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeatureDefaultEnabled": { + "TelemetryEnabled": true, + "Allocation": { + "DefaultWhenEnabled": "Medium", + "User": [ + { + "Variant": "Small", + "Users": [ + "Jeff" + ] + } ] - } - } - } - } - ] - }, - "CustomFilterFeature": { - "EnabledFor": [ - { - "Name": "CustomTargetingFilter", - "Parameters": { - "Audience": { - "Users": [ - "Jeff" - ] - } - } - } - ] - }, - "ConditionalFeature": { - "EnabledFor": [ - { - "Name": "Test", - "Parameters": { - "P1": "V1" - } - } - ] - }, - "ConditionalFeature2": { - "EnabledFor": [ - { - "Name": "Test" - } - ] - }, - "ContextualFeature": { - "EnabledFor": [ - { - "Name": "ContextualTest", - "Parameters": { - "AllowedAccounts": [ - "abc" + }, + "Variants": [ + { + "Name": "Medium", + "ConfigurationValue": { + "Size": "450px", + "Color": "Purple" + } + }, + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } ] - } - } - ] - }, - "AnyFilterFeature": { - "RequirementType": "Any", - "EnabledFor": [ - { - "Name": "Test", - "Parameters": { - "Id": "1" - } }, - { - "Name": "Test", - "Parameters": { - "Id": "2" - } - } - ] - }, - "AllFilterFeature": { - "RequirementType": "all", - "EnabledFor": [ - { - "Name": "Test", - "Parameters": { - "Id": "1" - } + "VariantFeatureUser": { + "TelemetryEnabled": true, + "Allocation": { + "User": [ + { + "Variant": "Small", + "Users": [ + "Marsha" + ] + } + ] + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] }, - { - "Name": "Test", - "Parameters": { - "Id": "2" - } - } - ] - - }, - "VariantFeaturePercentileOn": { - "Allocation": { - "Percentile": [ - { - "Variant": "Big", - "From": 0, - "To": 50 - } - ], - "Seed": 1234 - }, - "Variants": [ - { - "Name": "Big", - "ConfigurationReference": "ShoppingCart:Big", - "StatusOverride": "Disabled" - } - ], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeaturePercentileOff": { - "Allocation": { - "Percentile": [ - { - "Variant": "Big", - "From": 0, - "To": 50 - } - ], - "Seed": 12345 - }, - "Variants": [ - { - "Name": "Big", - "ConfigurationReference": "ShoppingCart:Big", - "StatusOverride": "Disabled" - } - ], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeatureStatusDisabled": { - "Status": "Disabled", - "TelemetryEnabled": true, - "Allocation": { - "DefaultWhenDisabled": "Small" - }, - "Variants": [ - { - "Name": "Small", - "ConfigurationValue": "300px" - } - ], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeatureDefaultEnabled": { - "TelemetryEnabled": true, - "Allocation": { - "DefaultWhenEnabled": "Medium", - "User": [ - { - "Variant": "Small", - "Users": [ - "Jeff" + "VariantFeatureGroup": { + "TelemetryEnabled": true, + "Allocation": { + "User": [ + { + "Variant": "Small", + "Users": [ + "Jeff" + ] + } + ], + "Group": [ + { + "Variant": "Small", + "Groups": [ + "Group1" + ] + } + ] + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } ] - } - ] - }, - "Variants": [ - { - "Name": "Medium", - "ConfigurationValue": { - "Size": "450px", - "Color": "Purple" - } }, - { - "Name": "Small", - "ConfigurationValue": "300px" - } - ], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeatureUser": { - "Allocation": { - "User": [ - { - "Variant": "Small", - "Users": [ - "Marsha" + "VariantFeatureNoVariants": { + "Allocation": { + "User": [ + { + "Variant": "Small", + "Users": [ + "Marsha" + ] + } + ] + }, + "Variants": [], + "EnabledFor": [ + { + "Name": "On" + } ] - } - ] - }, - "Variants": [ - { - "Name": "Small", - "ConfigurationValue": "300px" - } - ], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeatureGroup": { - "Allocation": { - "User": [ - { - "Variant": "Small", - "Users": [ - "Jeff" + }, + "VariantFeatureBothConfigurations": { + "Allocation": { + "DefaultWhenEnabled": "Small" + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "600px", + "ConfigurationReference": "ShoppingCart:Small" + } + ], + "EnabledFor": [ + { + "Name": "On" + } ] - } - ], - "Group": [ - { - "Variant": "Small", - "Groups": [ - "Group1" + }, + "VariantFeatureNoAllocation": { + "TelemetryEnabled": true, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } ] - } - ] - }, - "Variants": [ - { - "Name": "Small", - "ConfigurationValue": "300px" - } - ], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeatureNoVariants": { - "Allocation": { - "User": [ - { - "Variant": "Small", - "Users": [ - "Marsha" + }, + "VariantFeatureAlwaysOffNoAllocation": { + "TelemetryEnabled": true, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + ] + }, + "VariantFeatureInvalidStatusOverride": { + "Allocation": { + "DefaultWhenEnabled": "Small" + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px", + "StatusOverride": "InvalidValue" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeatureInvalidFromTo": { + "Allocation": { + "Percentile": [ + { + "Variant": "Small", + "From": "Invalid", + "To": "Invalid" + } + ] + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationReference": "ShoppingCart:Small" + } + ], + "EnabledFor": [ + { + "Name": "On" + } ] - } - ] - }, - "Variants": [], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeatureBothConfigurations": { - "Allocation": { - "DefaultWhenEnabled": "Small" - }, - "Variants": [ - { - "Name": "Small", - "ConfigurationValue": "600px", - "ConfigurationReference": "ShoppingCart:Small" - } - ], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeatureNoAllocation": { - "Variants": [ - { - "Name": "Small", - "ConfigurationValue": "300px" - } - ], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeatureInvalidStatusOverride": { - "Allocation": { - "DefaultWhenEnabled": "Small" - }, - "Variants": [ - { - "Name": "Small", - "ConfigurationValue": "300px", - "StatusOverride": "InvalidValue" - } - ], - "EnabledFor": [ - { - "Name": "On" - } - ] - }, - "VariantFeatureInvalidFromTo": { - "Allocation": { - "Percentile": [ - { - "Variant": "Small", - "From": "Invalid", - "To": "Invalid" - } - ] - }, - "Variants": [ - { - "Name": "Small", - "ConfigurationReference": "ShoppingCart:Small" - } - ], - "EnabledFor": [ - { - "Name": "On" } - ] } - } }