Skip to content
This repository has been archived by the owner on Oct 30, 2024. It is now read-only.

Commit

Permalink
prepare 5.4.0 release (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
eli-darkly authored Aug 30, 2018
1 parent d510782 commit fc225dc
Show file tree
Hide file tree
Showing 14 changed files with 1,031 additions and 192 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to the LaunchDarkly .NET SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).

## [5.4.0] - 2018-08-30
### Added:
- The new `LDClient` methods `BoolVariationDetail`, `IntVariationDetail`, `DoubleVariationDetail`, `StringVariationDetail`, and `JsonVariationDetail` allow you to evaluate a feature flag (using the same parameters as you would for `BoolVariation`, etc.) and receive more information about how the value was calculated. This information is returned in an `EvaluationDetail` object, which contains both the result value and an `EvaluationReason` which will tell you, for instance, if the user was individually targeted for the flag or was matched by one of the flag's rules, or if the flag returned the default value due to an error.

### Fixed:
- When evaluating a prerequisite feature flag, the analytics event for the evaluation did not include the result value if the prerequisite flag was off.

## [5.3.1] - 2018-08-30
### Fixed:
- Fixed a bug in streaming mode that prevented the client from reconnecting to the stream if it received an HTTP error status from the server (as opposed to simply losing the connection). ([#88](https://github.com/launchdarkly/dotnet-client/issues/88))
Expand Down
254 changes: 122 additions & 132 deletions src/LaunchDarkly.Client/FeatureFlag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,11 @@ internal FeatureFlag()

internal struct EvalResult
{
internal int? Variation;
internal JToken Result;
internal EvaluationDetail<JToken> Result;
internal readonly IList<FeatureRequestEvent> PrerequisiteEvents;

internal EvalResult(int? variation, JToken result, IList<FeatureRequestEvent> events) : this()
internal EvalResult(EvaluationDetail<JToken> result, IList<FeatureRequestEvent> events) : this()
{
Variation = variation;
Result = result;
PrerequisiteEvents = events;
}
Expand All @@ -83,149 +81,141 @@ internal EvalResult(int? variation, JToken result, IList<FeatureRequestEvent> ev
internal EvalResult Evaluate(User user, IFeatureStore featureStore, EventFactory eventFactory)
{
IList<FeatureRequestEvent> prereqEvents = new List<FeatureRequestEvent>();
EvalResult evalResult = new EvalResult(null, null, prereqEvents);
if (user == null || user.Key == null)
{
Log.WarnFormat("User or user key is null when evaluating flag: {0} returning null",
Key);

return evalResult;
}

if (On)
{
evalResult = Evaluate(user, featureStore, prereqEvents, eventFactory);
if (evalResult.Result != null)
{
return evalResult;
}
}
evalResult.Variation = OffVariation;
evalResult.Result = OffVariationValue;
return evalResult;
return new EvalResult(
new EvaluationDetail<JToken>(null, null, new EvaluationReason.Error(EvaluationErrorKind.USER_NOT_SPECIFIED)),
prereqEvents);
}
var details = Evaluate(user, featureStore, prereqEvents, eventFactory);
return new EvalResult(details, prereqEvents);
}

// Returning either a nil EvalResult or EvalResult.value indicates prereq failure/error.
private EvalResult Evaluate(User user, IFeatureStore featureStore, IList<FeatureRequestEvent> events,
private EvaluationDetail<JToken> Evaluate(User user, IFeatureStore featureStore, IList<FeatureRequestEvent> events,
EventFactory eventFactory)
{
var prereqOk = true;
if (Prerequisites != null)
{
foreach (var prereq in Prerequisites)
{
var prereqFeatureFlag = featureStore.Get(VersionedDataKind.Features, prereq.Key);
EvalResult prereqEvalResult = new EvalResult(null, null, events);
if (prereqFeatureFlag == null)
{
Log.ErrorFormat("Could not retrieve prerequisite flag: {0} when evaluating: {1}",
prereq.Key,
Key);
return new EvalResult(null, null, events);
}
else if (prereqFeatureFlag.On)
{
prereqEvalResult = prereqFeatureFlag.Evaluate(user, featureStore, events, eventFactory);
try
{
if (prereqEvalResult.Variation != prereq.Variation)
{
prereqOk = false;
}
}
catch (EvaluationException e)
{
Log.WarnFormat("Error evaluating prerequisites: {0}",
e,
Util.ExceptionMessage(e));

prereqOk = false;
}
}
else
{
prereqOk = false;
}
//We don't short circuit and also send events for each prereq.
events.Add(eventFactory.NewPrerequisiteFeatureRequestEvent(prereqFeatureFlag,
user, null, prereqEvalResult.Result, this));
}
}
if (prereqOk)
{
int? index = EvaluateIndex(user, featureStore);
JToken result = GetVariation(index);
return new EvalResult(index, result, events);
}
return new EvalResult(null, null, events);
}

private int? EvaluateIndex(User user, IFeatureStore store)
{
// Check to see if targets match
foreach (var target in Targets)
{
foreach (var v in target.Values)
{
if (v.Equals(user.Key))
{
return target.Variation;
}
}
}

// Now walk through the rules and see if any match
foreach (Rule rule in Rules)
{
if (rule.MatchesUser(user, store))
{
return rule.VariationIndexForUser(user, Key, Salt);
}
if (!On)
{
return GetOffValue(EvaluationReason.Off.Instance);
}

// Walk through the fallthrough and see if it matches
return Fallthrough.VariationIndexForUser(user, Key, Salt);
}

private JToken GetVariation(int? index)
var prereqFailureReason = CheckPrerequisites(user, featureStore, events, eventFactory);
if (prereqFailureReason != null)
{
return GetOffValue(prereqFailureReason);
}

// Check to see if targets match
if (Targets != null)
{
foreach (var target in Targets)
{
foreach (var v in target.Values)
{
if (user.Key == v)
{
return GetVariation(target.Variation, EvaluationReason.TargetMatch.Instance);
}
}
}
}
// Now walk through the rules and see if any match
if (Rules != null)
{
for (int i = 0; i < Rules.Count; i++)
{
Rule rule = Rules[i];
if (rule.MatchesUser(user, featureStore))
{
return GetValueForVariationOrRollout(rule, user,
new EvaluationReason.RuleMatch(i, rule.Id));
}
}
}
// Walk through the fallthrough and see if it matches
return GetValueForVariationOrRollout(Fallthrough, user, EvaluationReason.Fallthrough.Instance);
}

// Checks prerequisites if any; returns null if successful, or an EvaluationReason if we have to
// short-circuit due to a prerequisite failure.
private EvaluationReason CheckPrerequisites(User user, IFeatureStore featureStore, IList<FeatureRequestEvent> events,
EventFactory eventFactory)
{
// If the supplied index is null, then rules didn't match, and we want to return
// the off variation
if (index == null)
{
return null;
}
// If the index doesn't refer to a valid variation, that's an unexpected exception and we will
// return the default variation
else if (index >= Variations.Count)
{
throw new EvaluationException("Invalid index");
if (Prerequisites == null || Prerequisites.Count == 0)
{
return null;
}
else
{
return Variations[index.Value];
foreach (var prereq in Prerequisites)
{
var prereqOk = true;
var prereqFeatureFlag = featureStore.Get(VersionedDataKind.Features, prereq.Key);
if (prereqFeatureFlag == null)
{
Log.ErrorFormat("Could not retrieve prerequisite flag \"{0}\" when evaluating \"{1}\"",
prereq.Key, Key);
prereqOk = false;
}
else
{
var prereqEvalResult = prereqFeatureFlag.Evaluate(user, featureStore, events, eventFactory);
// Note that if the prerequisite flag is off, we don't consider it a match no matter
// what its off variation was. But we still need to evaluate it in order to generate
// an event.
if (!prereqFeatureFlag.On || prereqEvalResult.VariationIndex == null || prereqEvalResult.VariationIndex.Value != prereq.Variation)
{
prereqOk = false;
}
events.Add(eventFactory.NewPrerequisiteFeatureRequestEvent(prereqFeatureFlag, user,
prereqEvalResult, this));
}
if (!prereqOk)
{
return new EvaluationReason.PrerequisiteFailed(prereq.Key);
}
}
return null;
}

internal EvaluationDetail<JToken> ErrorResult(EvaluationErrorKind kind)
{
return new EvaluationDetail<JToken>(null, null, new EvaluationReason.Error(kind));
}

internal EvaluationDetail<JToken> GetVariation(int variation, EvaluationReason reason)
{
if (variation < 0 || variation >= Variations.Count)
{
Log.ErrorFormat("Data inconsistency in feature flag \"{0}\": invalid variation index", Key);
return ErrorResult(EvaluationErrorKind.MALFORMED_FLAG);
}
return new EvaluationDetail<JToken>(Variations[variation], variation, reason);
}

internal EvaluationDetail<JToken> GetOffValue(EvaluationReason reason)
{
if (OffVariation == null) // off variation unspecified - return default value
{
return new EvaluationDetail<JToken>(null, null, reason);
}
return GetVariation(OffVariation.Value, reason);
}

internal EvaluationDetail<JToken> GetValueForVariationOrRollout(VariationOrRollout vr,
User user, EvaluationReason reason)
{
var index = vr.VariationIndexForUser(user, Key, Salt);
if (index == null)
{
Log.ErrorFormat("Data inconsistency in feature flag \"{0}\": variation/rollout object with no variation or rollout", Key);
return ErrorResult(EvaluationErrorKind.MALFORMED_FLAG);
}
return GetVariation(index.Value, reason);
}

internal JToken OffVariationValue
{
get
{
if (!OffVariation.HasValue)
{
return null;
}

if (OffVariation.Value >= Variations.Count)
{
throw new EvaluationException("Invalid off variation index");
}

return Variations[OffVariation.Value];
}
}
}

}

internal class Rollout
{
[JsonProperty(PropertyName = "variations")]
Expand Down
31 changes: 26 additions & 5 deletions src/LaunchDarkly.Client/FeatureFlagsState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ internal FeatureFlagsState(bool valid, IDictionary<string, JToken> values,
_flagMetadata = metadata;
}

internal void AddFlag(FeatureFlag flag, JToken value, int? variation)
internal void AddFlag(FeatureFlag flag, JToken value, int? variation, EvaluationReason reason)
{
_flagValues[flag.Key] = value;
_flagMetadata[flag.Key] = new FlagMetadata
{
Variation = variation,
Version = flag.Version,
TrackEvents = flag.TrackEvents,
DebugEventsUntilDate = flag.DebugEventsUntilDate
DebugEventsUntilDate = flag.DebugEventsUntilDate,
Reason = reason
};
}

Expand All @@ -68,6 +69,23 @@ public JToken GetFlagValue(string key)
return null;
}

/// <summary>
/// Returns the evaluation reason of an individual feature flag (as returned by
/// <see cref="ILdClient.BoolVariation(string, User, bool)"/>, etc.) at the time the state
/// was recorded.
/// </summary>
/// <param name="key">the feature flag key</param>
/// <returns>the evaluation reason; null if reasons were not recorded, or if there was no
/// such flag</returns>
public EvaluationReason GetFlagReason(string key)
{
if (_flagMetadata.TryGetValue(key, out var meta))
{
return meta.Reason;
}
return null;
}

/// <summary>
/// Returns a dictionary of flag keys to flag values. If a flag would have evaluated to the
/// default value, its value will be null.
Expand Down Expand Up @@ -111,6 +129,8 @@ internal class FlagMetadata
internal bool TrackEvents { get; set; }
[JsonProperty(PropertyName = "debugEventsUntilDate", NullValueHandling = NullValueHandling.Ignore)]
internal long? DebugEventsUntilDate { get; set; }
[JsonProperty(PropertyName = "reason", NullValueHandling = NullValueHandling.Ignore)]
internal EvaluationReason Reason { get; set; }

public override bool Equals(object other)
{
Expand All @@ -119,15 +139,16 @@ public override bool Equals(object other)
return Variation == o.Variation &&
Version == o.Version &&
TrackEvents == o.TrackEvents &&
DebugEventsUntilDate == o.DebugEventsUntilDate;
DebugEventsUntilDate == o.DebugEventsUntilDate &&
Object.Equals(Reason, o.Reason);
}
return false;
}

public override int GetHashCode()
{
return ((((Variation.GetHashCode() * 17) + Version.GetHashCode()) * 17) + TrackEvents.GetHashCode()) * 17 +
DebugEventsUntilDate.GetHashCode();
return (((((Variation.GetHashCode() * 17) + Version.GetHashCode()) * 17) + TrackEvents.GetHashCode()) * 17 +
DebugEventsUntilDate.GetHashCode()) * 17 + (Reason == null ? 0 : Reason.GetHashCode());
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/LaunchDarkly.Client/FlagsStateOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public override string ToString()
/// </summary>
public static readonly FlagsStateOption ClientSideOnly = new FlagsStateOption("ClientSideOnly");

/// <summary>
/// Specifies that evaluation reasons should be included in the state object (as returned by
/// <see cref="ILdClient.BoolVariationDetail(string, User, bool)"/>, etc.). By default, they
/// are not included.
/// </summary>
public static readonly FlagsStateOption WithReasons = new FlagsStateOption("WithReasons");

internal static bool HasOption(FlagsStateOption[] options, FlagsStateOption option)
{
foreach (var o in options)
Expand Down
Loading

0 comments on commit fc225dc

Please sign in to comment.