Skip to content
Draft
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
59 changes: 48 additions & 11 deletions pkgs/shared/common/src/EvaluationDetail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ public override int GetHashCode() =>
public struct EvaluationReason : IJsonSerializable
{
private static readonly EvaluationReason _offInstance =
new EvaluationReason(EvaluationReasonKind.Off, null, null, null, null, false, null);
new EvaluationReason(EvaluationReasonKind.Off, null, null, null, null, false, null, false);
private static readonly EvaluationReason _fallthroughInstance =
new EvaluationReason(EvaluationReasonKind.Fallthrough, null, null, null, null, false, null);
new EvaluationReason(EvaluationReasonKind.Fallthrough, null, null, null, null, false, null, false);
private static readonly EvaluationReason _targetMatchInstance =
new EvaluationReason(EvaluationReasonKind.TargetMatch, null, null, null, null, false, null);
new EvaluationReason(EvaluationReasonKind.TargetMatch, null, null, null, null, false, null, false);

private readonly EvaluationReasonKind _kind;
private readonly int? _ruleIndex;
Expand All @@ -83,6 +83,7 @@ public struct EvaluationReason : IJsonSerializable
private readonly EvaluationErrorKind? _errorKind;
private readonly bool _inExperiment;
private readonly BigSegmentsStatus? _bigSegmentsStatus;
private readonly bool _overrideAffected;

/// <summary>
/// An enum indicating the general category of the reason.
Expand Down Expand Up @@ -130,14 +131,34 @@ public struct EvaluationReason : IJsonSerializable
/// </remarks>
public BigSegmentsStatus? BigSegmentsStatus => _bigSegmentsStatus;

/// <summary>
/// Whether a flag override affected this evaluation, directly or transitively.
/// </summary>
/// <remarks>
/// <para>
/// This is true if the evaluated flag came from the SDK's override store. It is also true if a
/// prerequisite flag at any depth, or a segment read during the evaluation, came from that store.
/// Otherwise it is false.
/// </para>
/// <para>
/// In the JSON representation, the <c>overrideAffected</c> property appears only when this value
/// is true.
/// </para>
/// <para>
/// Flag overrides are currently experimental and subject to change.
/// </para>
/// </remarks>
public bool OverrideAffected => _overrideAffected;

internal EvaluationReason(
EvaluationReasonKind kind,
int? ruleIndex,
string ruleId,
string prereqKey,
EvaluationErrorKind? errorKind,
bool inExperiment,
BigSegmentsStatus? bigSegmentsStatus
BigSegmentsStatus? bigSegmentsStatus,
bool overrideAffected
)
{
_kind = kind;
Expand All @@ -147,6 +168,7 @@ internal EvaluationReason(
_errorKind = errorKind;
_inExperiment = inExperiment;
_bigSegmentsStatus = bigSegmentsStatus;
_overrideAffected = overrideAffected;
}

/// <summary>
Expand All @@ -171,23 +193,23 @@ internal EvaluationReason(
/// <param name="ruleId">the unique rule ID</param>
/// <returns>a reason descriptor</returns>
public static EvaluationReason RuleMatchReason(int ruleIndex, string ruleId) =>
new EvaluationReason(EvaluationReasonKind.RuleMatch, ruleIndex, ruleId, null, null, false, null);
new EvaluationReason(EvaluationReasonKind.RuleMatch, ruleIndex, ruleId, null, null, false, null, false);

/// <summary>
/// Returns an EvaluationReason of the kind <see cref="EvaluationReasonKind.PrerequisiteFailed"/>.
/// </summary>
/// <param name="key">the key of the prerequisite flag</param>
/// <returns>a reason descriptor</returns>
public static EvaluationReason PrerequisiteFailedReason(string key) =>
new EvaluationReason(EvaluationReasonKind.PrerequisiteFailed, null, null, key, null, false, null);
new EvaluationReason(EvaluationReasonKind.PrerequisiteFailed, null, null, key, null, false, null, false);

/// <summary>
/// Returns an EvaluationReason of the kind <see cref="EvaluationReasonKind.Error"/>.
/// </summary>
/// <param name="errorKind"></param>
/// <returns>a reason descriptor</returns>
public static EvaluationReason ErrorReason(EvaluationErrorKind errorKind) =>
new EvaluationReason(EvaluationReasonKind.Error, null, null, null, errorKind, false, null);
new EvaluationReason(EvaluationReasonKind.Error, null, null, null, errorKind, false, null, false);

/// <summary>
/// Returns a copy of this EvaluationReason with a specific <see cref="BigSegmentsStatus"/> value added.
Expand All @@ -196,7 +218,20 @@ public static EvaluationReason ErrorReason(EvaluationErrorKind errorKind) =>
/// <returns>a reason descriptor</returns>
public EvaluationReason WithBigSegmentsStatus(BigSegmentsStatus? bigSegmentsStatus) =>
new EvaluationReason(_kind, _ruleIndex, _ruleId, _prerequisiteKey, _errorKind,
_inExperiment, bigSegmentsStatus);
_inExperiment, bigSegmentsStatus, _overrideAffected);

/// <summary>
/// Returns a copy of this EvaluationReason with a specific <see cref="OverrideAffected"/> value.
/// </summary>
/// <remarks>
/// The copy keeps all other properties of this reason. Flag overrides are currently experimental
/// and subject to change.
/// </remarks>
/// <param name="overrideAffected">the new property value</param>
/// <returns>a reason descriptor</returns>
public EvaluationReason WithOverrideAffected(bool overrideAffected) =>
new EvaluationReason(_kind, _ruleIndex, _ruleId, _prerequisiteKey, _errorKind,
_inExperiment, _bigSegmentsStatus, overrideAffected);

/// <summary>
/// Returns a new instance with the <see cref="InExperiment"/> property set to the specified
Expand All @@ -214,7 +249,8 @@ public EvaluationReason WithInExperiment(bool inExperiment)
{
case EvaluationReasonKind.Fallthrough:
case EvaluationReasonKind.RuleMatch:
return new EvaluationReason(_kind, _ruleIndex, _ruleId, _prerequisiteKey, _errorKind, inExperiment, _bigSegmentsStatus);
return new EvaluationReason(_kind, _ruleIndex, _ruleId, _prerequisiteKey, _errorKind, inExperiment,
_bigSegmentsStatus, _overrideAffected);
default:
return this;
}
Expand All @@ -225,12 +261,13 @@ public override bool Equals(object obj) =>
obj is EvaluationReason o &&
_kind == o._kind && _ruleId == o._ruleId && _ruleIndex == o._ruleIndex &&
_prerequisiteKey == o._prerequisiteKey && _errorKind == o._errorKind &&
_inExperiment == o._inExperiment && _bigSegmentsStatus == o._bigSegmentsStatus;
_inExperiment == o._inExperiment && _bigSegmentsStatus == o._bigSegmentsStatus &&
_overrideAffected == o._overrideAffected;

/// <inheritdoc/>
public override int GetHashCode() =>
new HashCodeBuilder().With(_kind).With(_ruleIndex).With(_ruleId).With(_prerequisiteKey)
.With(_errorKind).With(_inExperiment).With(_bigSegmentsStatus).Value;
.With(_errorKind).With(_inExperiment).With(_bigSegmentsStatus).With(_overrideAffected).Value;

/// <inheritdoc/>
public override string ToString()
Expand Down
12 changes: 12 additions & 0 deletions pkgs/shared/common/src/Json/LdJsonConverters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public static EvaluationReason ReadJsonValue(ref Utf8JsonReader reader)
string prerequisiteKey = null;
EvaluationErrorKind? errorKind = null;
bool inExperiment = false;
bool overrideAffected = false;
BigSegmentsStatus? bigSegmentsStatus = null;

RequireToken(ref reader, JsonTokenType.StartObject);
Expand All @@ -99,6 +100,9 @@ public static EvaluationReason ReadJsonValue(ref Utf8JsonReader reader)
case "inExperiment":
inExperiment = reader.GetBoolean();
break;
case "overrideAffected":
overrideAffected = reader.GetBoolean();
break;
case "bigSegmentsStatus":
bigSegmentsStatus = BigSegmentsStatusConverter.FromIdentifier(reader.GetString());
break;
Expand Down Expand Up @@ -146,6 +150,10 @@ public static EvaluationReason ReadJsonValue(ref Utf8JsonReader reader)
{
reason = reason.WithBigSegmentsStatus(bigSegmentsStatus);
}
if (overrideAffected)
{
reason = reason.WithOverrideAffected(true);
}
return reason;
}

Expand All @@ -171,6 +179,10 @@ public static void WriteJsonValue(EvaluationReason value, Utf8JsonWriter writer)
{
writer.WriteBoolean("inExperiment", true); // omit property if false
}
if (value.OverrideAffected)
{
writer.WriteBoolean("overrideAffected", true); // omit property if false
}
if (value.BigSegmentsStatus.HasValue)
{
writer.WritePropertyName("bigSegmentsStatus");
Expand Down
76 changes: 76 additions & 0 deletions pkgs/shared/common/test/EvaluationDetailTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ public void TestReasonSerializationDeserialization()
new ReasonTestCase { Reason = EvaluationReason.ErrorReason(EvaluationErrorKind.Exception),
JsonString = @"{""kind"":""ERROR"",""errorKind"":""EXCEPTION""}",
ExpectedShortString = "ERROR(EXCEPTION)"
},
new ReasonTestCase { Reason = EvaluationReason.OffReason.WithOverrideAffected(true),
JsonString = @"{""kind"":""OFF"",""overrideAffected"":true}", ExpectedShortString = "OFF" },
new ReasonTestCase {
Reason = EvaluationReason.RuleMatchReason(1, "id").WithInExperiment(true).WithOverrideAffected(true),
JsonString = @"{""kind"":""RULE_MATCH"",""ruleIndex"":1,""ruleId"":""id"",""inExperiment"":true,""overrideAffected"":true}",
ExpectedShortString = "RULE_MATCH(1,id)"
},
new ReasonTestCase {
Reason = EvaluationReason.FallthroughReason.WithBigSegmentsStatus(BigSegmentsStatus.Healthy).WithOverrideAffected(true),
JsonString = @"{""kind"":""FALLTHROUGH"",""bigSegmentsStatus"":""HEALTHY"",""overrideAffected"":true}",
ExpectedShortString = "FALLTHROUGH" },
new ReasonTestCase { Reason = EvaluationReason.PrerequisiteFailedReason("key").WithOverrideAffected(true),
JsonString = @"{""kind"":""PREREQUISITE_FAILED"",""prerequisiteKey"":""key"",""overrideAffected"":true}",
ExpectedShortString = "PREREQUISITE_FAILED(key)"
},
new ReasonTestCase { Reason = EvaluationReason.ErrorReason(EvaluationErrorKind.MalformedFlag).WithOverrideAffected(true),
JsonString = @"{""kind"":""ERROR"",""errorKind"":""MALFORMED_FLAG"",""overrideAffected"":true}",
ExpectedShortString = "ERROR(MALFORMED_FLAG)"
}
})
{
Expand All @@ -81,6 +100,60 @@ public void TestReasonSerializationDeserialization()
}
}

[Fact]
public void OverrideAffectedIsFalseByDefault()
{
Assert.False(EvaluationReason.OffReason.OverrideAffected);
Assert.False(EvaluationReason.FallthroughReason.OverrideAffected);
Assert.False(EvaluationReason.TargetMatchReason.OverrideAffected);
Assert.False(EvaluationReason.RuleMatchReason(0, "id").OverrideAffected);
Assert.False(EvaluationReason.PrerequisiteFailedReason("key").OverrideAffected);
Assert.False(EvaluationReason.ErrorReason(EvaluationErrorKind.FlagNotFound).OverrideAffected);
}

[Fact]
public void WithOverrideAffectedKeepsOtherProperties()
{
var reason = EvaluationReason.RuleMatchReason(2, "rule").WithInExperiment(true)
.WithBigSegmentsStatus(BigSegmentsStatus.Stale);
var marked = reason.WithOverrideAffected(true);
Assert.True(marked.OverrideAffected);
Assert.Equal(EvaluationReasonKind.RuleMatch, marked.Kind);
Assert.Equal(2, marked.RuleIndex);
Assert.Equal("rule", marked.RuleId);
Assert.True(marked.InExperiment);
Assert.Equal(BigSegmentsStatus.Stale, marked.BigSegmentsStatus);

Assert.False(marked.WithOverrideAffected(false).OverrideAffected);
Assert.Equal(reason, marked.WithOverrideAffected(false));
}

[Fact]
public void OtherCopyMethodsKeepOverrideAffected()
{
var marked = EvaluationReason.FallthroughReason.WithOverrideAffected(true);
Assert.True(marked.WithInExperiment(true).OverrideAffected);
Assert.True(marked.WithBigSegmentsStatus(BigSegmentsStatus.Healthy).OverrideAffected);
}

[Fact]
public void OverrideAffectedIsOmittedFromJsonWhenFalse()
{
var json = LdValue.Parse(LdJsonSerialization.SerializeObject(EvaluationReason.OffReason));
Assert.Equal(LdValue.Null, json.Get("overrideAffected"));

var explicitFalse = EvaluationReason.OffReason.WithOverrideAffected(true).WithOverrideAffected(false);
json = LdValue.Parse(LdJsonSerialization.SerializeObject(explicitFalse));
Assert.Equal(LdValue.Null, json.Get("overrideAffected"));
}

[Fact]
public void OverrideAffectedFalseInJsonIsAccepted()
{
var reason = LdJsonSerialization.DeserializeObject<EvaluationReason>(@"{""kind"":""OFF"",""overrideAffected"":false}");
Assert.Equal(EvaluationReason.OffReason, reason);
}

[Fact]
public void TestBigSegmentsStatusSerializationDeserialization()
{
Expand Down Expand Up @@ -127,7 +200,9 @@ public void TestEqualityAndHashCode()
TypeBehavior.CheckEqualsAndHashCode(
// each value in this list should be unequal to all the other values and equal to itself
() => EvaluationReason.OffReason,
() => EvaluationReason.OffReason.WithOverrideAffected(true),
() => EvaluationReason.FallthroughReason,
() => EvaluationReason.FallthroughReason.WithOverrideAffected(true),
() => EvaluationReason.FallthroughReason.WithInExperiment(true),
() => EvaluationReason.RuleMatchReason(0, "rule1"),
() => EvaluationReason.RuleMatchReason(0, "rule1").WithInExperiment(true),
Expand All @@ -136,6 +211,7 @@ public void TestEqualityAndHashCode()
() => EvaluationReason.PrerequisiteFailedReason("a"),
() => EvaluationReason.PrerequisiteFailedReason("b"),
() => EvaluationReason.ErrorReason(EvaluationErrorKind.FlagNotFound),
() => EvaluationReason.ErrorReason(EvaluationErrorKind.FlagNotFound).WithOverrideAffected(true),
() => EvaluationReason.ErrorReason(EvaluationErrorKind.Exception)
);
}
Expand Down
Loading