Skip to content

Commit 9762c32

Browse files
committed
Adds Reason field and adjusts evaluation logic
1 parent 197c325 commit 9762c32

File tree

3 files changed

+130
-115
lines changed

3 files changed

+130
-115
lines changed

src/Microsoft.FeatureManagement/FeatureManager.cs

Lines changed: 112 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -71,92 +71,148 @@ public FeatureManager(
7171

7272
public Task<bool> IsEnabledAsync(string feature)
7373
{
74-
return IsEnabledWithVariantsAsync<object>(feature, appContext: null, useAppContext: false, CancellationToken.None);
74+
return IsEnabledEvaluation<object>(feature, appContext: null, useAppContext: false, CancellationToken.None);
7575
}
7676

7777
public Task<bool> IsEnabledAsync<TContext>(string feature, TContext appContext)
7878
{
79-
return IsEnabledWithVariantsAsync(feature, appContext, useAppContext: true, CancellationToken.None);
79+
return IsEnabledEvaluation(feature, appContext, useAppContext: true, CancellationToken.None);
8080
}
8181

8282
public Task<bool> IsEnabledAsync(string feature, CancellationToken cancellationToken)
8383
{
84-
return IsEnabledWithVariantsAsync<object>(feature, appContext: null, useAppContext: false, cancellationToken);
84+
return IsEnabledEvaluation<object>(feature, appContext: null, useAppContext: false, cancellationToken);
8585
}
8686

8787
public Task<bool> IsEnabledAsync<TContext>(string feature, TContext appContext, CancellationToken cancellationToken)
8888
{
89-
return IsEnabledWithVariantsAsync(feature, appContext, useAppContext: true, cancellationToken);
89+
return IsEnabledEvaluation(feature, appContext, useAppContext: true, cancellationToken);
9090
}
9191

92-
private async Task<bool> IsEnabledWithVariantsAsync<TContext>(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken)
92+
private async Task<bool> IsEnabledEvaluation<TContext>(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken)
9393
{
94-
bool isFeatureEnabled = false;
94+
EvaluationEvent evaluationEvent = new EvaluationEvent
95+
{
96+
FeatureDefinition = await GetFeatureDefinition(feature).ConfigureAwait(false)
97+
};
9598

96-
FeatureDefinition featureDefinition = await GetFeatureDefinition(feature).ConfigureAwait(false);
99+
await EvaluateFeature(evaluationEvent, appContext, useAppContext, cancellationToken);
97100

98-
VariantDefinition variantDefinition = null;
101+
return evaluationEvent.IsEnabled;
102+
}
99103

100-
if (featureDefinition != null)
104+
public ValueTask<Variant> GetVariantAsync(string feature, CancellationToken cancellationToken)
105+
{
106+
if (string.IsNullOrEmpty(feature))
101107
{
102-
isFeatureEnabled = await IsEnabledAsync(featureDefinition, appContext, useAppContext, cancellationToken).ConfigureAwait(false);
108+
throw new ArgumentNullException(nameof(feature));
109+
}
110+
111+
return GetVariantEvaluation(feature, context: null, useContext: false, cancellationToken);
112+
}
113+
114+
public ValueTask<Variant> GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken)
115+
{
116+
if (string.IsNullOrEmpty(feature))
117+
{
118+
throw new ArgumentNullException(nameof(feature));
119+
}
120+
121+
if (context == null)
122+
{
123+
throw new ArgumentNullException(nameof(context));
124+
}
125+
126+
return GetVariantEvaluation(feature, context, useContext: true, cancellationToken);
127+
}
103128

104-
if (featureDefinition.Variants != null && featureDefinition.Variants.Any() && featureDefinition.Allocation != null)
129+
private async ValueTask<Variant> GetVariantEvaluation(string feature, TargetingContext context, bool useContext, CancellationToken cancellationToken)
130+
{
131+
EvaluationEvent evaluationEvent = new EvaluationEvent
132+
{
133+
FeatureDefinition = await GetFeatureDefinition(feature).ConfigureAwait(false)
134+
};
135+
136+
await EvaluateFeature(evaluationEvent, context, useContext, cancellationToken);
137+
138+
return evaluationEvent.Variant;
139+
}
140+
141+
private async Task<EvaluationEvent> EvaluateFeature<TContext>(EvaluationEvent evaluationEvent, TContext context, bool useContext, CancellationToken cancellationToken)
142+
{
143+
if (evaluationEvent.FeatureDefinition != null)
144+
{
145+
//
146+
// Determine IsEnabled
147+
evaluationEvent.IsEnabled = await IsEnabledAsync(evaluationEvent.FeatureDefinition, context, useContext, cancellationToken).ConfigureAwait(false);
148+
149+
//
150+
// Determine Variant
151+
VariantDefinition variantDefinition;
152+
153+
if (evaluationEvent.FeatureDefinition.Allocation == null || (!evaluationEvent.FeatureDefinition.Variants?.Any() ?? false))
154+
{
155+
variantDefinition = null;
156+
157+
evaluationEvent.VariantReason = "No Allocation or Variants";
158+
}
159+
else
105160
{
106-
if (!isFeatureEnabled)
161+
if (!evaluationEvent.IsEnabled)
107162
{
108-
variantDefinition = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == featureDefinition.Allocation.DefaultWhenDisabled);
163+
variantDefinition = evaluationEvent.FeatureDefinition.Variants.FirstOrDefault((variant) => variant.Name == evaluationEvent.FeatureDefinition.Allocation.DefaultWhenDisabled);
164+
165+
evaluationEvent.VariantReason = "Disabled Default";
109166
}
110167
else
111168
{
112169
TargetingContext targetingContext;
113170

114-
if (useAppContext)
171+
if (useContext)
115172
{
116-
targetingContext = appContext as TargetingContext;
173+
targetingContext = context as TargetingContext;
117174
}
118175
else
119176
{
120177
targetingContext = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false);
121178
}
122179

123180
variantDefinition = await GetAssignedVariantAsync(
124-
featureDefinition,
181+
evaluationEvent,
125182
targetingContext,
126183
cancellationToken)
127184
.ConfigureAwait(false);
128185
}
129186

130-
if (variantDefinition != null && featureDefinition.Status != FeatureStatus.Disabled)
187+
evaluationEvent.Variant = variantDefinition != null ? GetVariantFromVariantDefinition(variantDefinition) : null;
188+
189+
//
190+
// Override IsEnabled if variant has an override
191+
if (variantDefinition != null && evaluationEvent.FeatureDefinition.Status != FeatureStatus.Disabled)
131192
{
132193
if (variantDefinition.StatusOverride == StatusOverride.Enabled)
133194
{
134-
isFeatureEnabled = true;
195+
evaluationEvent.IsEnabled = true;
135196
}
136197
else if (variantDefinition.StatusOverride == StatusOverride.Disabled)
137198
{
138-
isFeatureEnabled = false;
199+
evaluationEvent.IsEnabled = false;
139200
}
140201
}
141202
}
142203
}
143204

144205
foreach (ISessionManager sessionManager in _sessionManagers)
145206
{
146-
await sessionManager.SetAsync(feature, isFeatureEnabled).ConfigureAwait(false);
207+
await sessionManager.SetAsync(evaluationEvent.FeatureDefinition.Name, evaluationEvent.IsEnabled).ConfigureAwait(false);
147208
}
148209

149-
if (featureDefinition.TelemetryEnabled)
210+
if (evaluationEvent.FeatureDefinition.TelemetryEnabled)
150211
{
151-
PublishTelemetry(new EvaluationEvent
152-
{
153-
FeatureDefinition = featureDefinition,
154-
IsEnabled = isFeatureEnabled,
155-
Variant = variantDefinition != null ? GetVariantFromVariantDefinition(variantDefinition) : null
156-
}, cancellationToken);
212+
PublishTelemetry(evaluationEvent, cancellationToken);
157213
}
158214

159-
return isFeatureEnabled;
215+
return evaluationEvent;
160216
}
161217

162218
public IAsyncEnumerable<string> GetFeatureNamesAsync()
@@ -304,73 +360,6 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false)
304360
return enabled;
305361
}
306362

307-
public ValueTask<Variant> GetVariantAsync(string feature, CancellationToken cancellationToken)
308-
{
309-
if (string.IsNullOrEmpty(feature))
310-
{
311-
throw new ArgumentNullException(nameof(feature));
312-
}
313-
314-
return GetVariantAsync(feature, context: null, useContext: false, cancellationToken);
315-
}
316-
317-
public ValueTask<Variant> GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken)
318-
{
319-
if (string.IsNullOrEmpty(feature))
320-
{
321-
throw new ArgumentNullException(nameof(feature));
322-
}
323-
324-
if (context == null)
325-
{
326-
throw new ArgumentNullException(nameof(context));
327-
}
328-
329-
return GetVariantAsync(feature, context, useContext: true, cancellationToken);
330-
}
331-
332-
private async ValueTask<Variant> GetVariantAsync(string feature, TargetingContext context, bool useContext, CancellationToken cancellationToken)
333-
{
334-
FeatureDefinition featureDefinition = await GetFeatureDefinition(feature).ConfigureAwait(false);
335-
336-
if (featureDefinition == null || featureDefinition.Allocation == null || (!featureDefinition.Variants?.Any() ?? false))
337-
{
338-
return null;
339-
}
340-
341-
VariantDefinition variantDefinition = null;
342-
343-
bool isFeatureEnabled = await IsEnabledAsync(featureDefinition, context, useContext, cancellationToken).ConfigureAwait(false);
344-
345-
if (!isFeatureEnabled)
346-
{
347-
variantDefinition = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == featureDefinition.Allocation.DefaultWhenDisabled);
348-
}
349-
else
350-
{
351-
if (!useContext)
352-
{
353-
context = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false);
354-
}
355-
356-
variantDefinition = await GetAssignedVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false);
357-
}
358-
359-
Variant variant = variantDefinition != null ? GetVariantFromVariantDefinition(variantDefinition) : null;
360-
361-
if (featureDefinition.TelemetryEnabled)
362-
{
363-
PublishTelemetry(new EvaluationEvent
364-
{
365-
FeatureDefinition = featureDefinition,
366-
IsEnabled = isFeatureEnabled,
367-
Variant = variant
368-
}, cancellationToken);
369-
}
370-
371-
return variant;
372-
}
373-
374363
private async ValueTask<FeatureDefinition> GetFeatureDefinition(string feature)
375364
{
376365
FeatureDefinition featureDefinition = await _featureDefinitionProvider
@@ -415,95 +404,103 @@ private async ValueTask<TargetingContext> ResolveTargetingContextAsync(Cancellat
415404
return context;
416405
}
417406

418-
private async ValueTask<VariantDefinition> GetAssignedVariantAsync(FeatureDefinition featureDefinition, TargetingContext context, CancellationToken cancellationToken)
407+
private async ValueTask<VariantDefinition> GetAssignedVariantAsync(EvaluationEvent evaluationEvent, TargetingContext context, CancellationToken cancellationToken)
419408
{
420409
VariantDefinition variantDefinition = null;
421410

422411
if (context != null)
423412
{
424-
variantDefinition = await AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false);
413+
variantDefinition = await AssignVariantAsync(evaluationEvent, context, cancellationToken).ConfigureAwait(false);
425414
}
426415

427416
if (variantDefinition == null)
428417
{
429-
variantDefinition = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == featureDefinition.Allocation.DefaultWhenEnabled);
418+
variantDefinition = evaluationEvent.FeatureDefinition.Variants.FirstOrDefault((variant) => variant.Name == evaluationEvent.FeatureDefinition.Allocation.DefaultWhenEnabled);
419+
420+
evaluationEvent.VariantReason = "Enabled Default";
430421
}
431422

432423
return variantDefinition;
433424
}
434425

435-
private ValueTask<VariantDefinition> AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken)
426+
private ValueTask<VariantDefinition> AssignVariantAsync(EvaluationEvent evaluationEvent, TargetingContext targetingContext, CancellationToken cancellationToken)
436427
{
437428
VariantDefinition variant = null;
438429

439-
if (featureDefinition.Allocation.User != null)
430+
if (evaluationEvent.FeatureDefinition.Allocation.User != null)
440431
{
441-
foreach (UserAllocation user in featureDefinition.Allocation.User)
432+
foreach (UserAllocation user in evaluationEvent.FeatureDefinition.Allocation.User)
442433
{
443434
if (TargetingEvaluator.IsTargeted(targetingContext.UserId, user.Users, _assignerOptions.IgnoreCase))
444435
{
445436
if (string.IsNullOrEmpty(user.Variant))
446437
{
447-
_logger.LogWarning($"Missing variant name for user allocation in feature {featureDefinition.Name}");
438+
_logger.LogWarning($"Missing variant name for user allocation in feature {evaluationEvent.FeatureDefinition.Name}");
448439

449440
return new ValueTask<VariantDefinition>((VariantDefinition)null);
450441
}
451442

452-
Debug.Assert(featureDefinition.Variants != null);
443+
Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null);
444+
445+
evaluationEvent.VariantReason = "User Allocated";
453446

454447
return new ValueTask<VariantDefinition>(
455-
featureDefinition
448+
evaluationEvent.FeatureDefinition
456449
.Variants
457450
.FirstOrDefault((variant) => variant.Name == user.Variant));
458451
}
459452
}
460453
}
461454

462-
if (featureDefinition.Allocation.Group != null)
455+
if (evaluationEvent.FeatureDefinition.Allocation.Group != null)
463456
{
464-
foreach (GroupAllocation group in featureDefinition.Allocation.Group)
457+
foreach (GroupAllocation group in evaluationEvent.FeatureDefinition.Allocation.Group)
465458
{
466459
if (TargetingEvaluator.IsTargeted(targetingContext.Groups, group.Groups, _assignerOptions.IgnoreCase))
467460
{
468461
if (string.IsNullOrEmpty(group.Variant))
469462
{
470-
_logger.LogWarning($"Missing variant name for group allocation in feature {featureDefinition.Name}");
463+
_logger.LogWarning($"Missing variant name for group allocation in feature {evaluationEvent.FeatureDefinition.Name}");
471464

472465
return new ValueTask<VariantDefinition>((VariantDefinition)null);
473466
}
474467

475-
Debug.Assert(featureDefinition.Variants != null);
468+
Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null);
469+
470+
evaluationEvent.VariantReason = "Group Allocated";
476471

477472
return new ValueTask<VariantDefinition>(
478-
featureDefinition
473+
evaluationEvent.FeatureDefinition
479474
.Variants
480475
.FirstOrDefault((variant) => variant.Name == group.Variant));
481476
}
482477
}
483478
}
484479

485-
if (featureDefinition.Allocation.Percentile != null)
480+
if (evaluationEvent.FeatureDefinition.Allocation.Percentile != null)
486481
{
487-
foreach (PercentileAllocation percentile in featureDefinition.Allocation.Percentile)
482+
foreach (PercentileAllocation percentile in evaluationEvent.FeatureDefinition.Allocation.Percentile)
488483
{
489484
if (TargetingEvaluator.IsTargeted(
490485
targetingContext,
491486
percentile.From,
492487
percentile.To,
493488
_assignerOptions.IgnoreCase,
494-
featureDefinition.Allocation.Seed ?? $"allocation\n{featureDefinition.Name}"))
489+
evaluationEvent.FeatureDefinition.Allocation.Seed ?? $"allocation\n{evaluationEvent.FeatureDefinition.Name}"))
495490
{
496491
if (string.IsNullOrEmpty(percentile.Variant))
497492
{
498-
_logger.LogWarning($"Missing variant name for percentile allocation in feature {featureDefinition.Name}");
493+
_logger.LogWarning($"Missing variant name for percentile allocation in feature {evaluationEvent.FeatureDefinition.Name}");
499494

500495
return new ValueTask<VariantDefinition>((VariantDefinition)null);
501496
}
502497

503-
Debug.Assert(featureDefinition.Variants != null);
498+
Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null);
499+
500+
evaluationEvent.VariantReason = "Percentile Allocated";
504501

505502
return new ValueTask<VariantDefinition>(
506-
featureDefinition
503+
evaluationEvent.FeatureDefinition
507504
.Variants
508505
.FirstOrDefault((variant) => variant.Name == percentile.Variant));
509506
}

src/Microsoft.FeatureManagement/Telemetry/EvaluationEvent.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,10 @@ public class EvaluationEvent
2424
/// The variant given after evaluation.
2525
/// </summary>
2626
public Variant Variant { get; set; }
27+
28+
/// <summary>
29+
/// The reason the variant was given.
30+
/// </summary>
31+
public string VariantReason { get; set; }
2732
}
2833
}

0 commit comments

Comments
 (0)