diff --git a/pkgs/shared/common/src/EvaluationDetail.cs b/pkgs/shared/common/src/EvaluationDetail.cs index e27a8c1e5..36782cebc 100644 --- a/pkgs/shared/common/src/EvaluationDetail.cs +++ b/pkgs/shared/common/src/EvaluationDetail.cs @@ -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; @@ -83,6 +83,7 @@ public struct EvaluationReason : IJsonSerializable private readonly EvaluationErrorKind? _errorKind; private readonly bool _inExperiment; private readonly BigSegmentsStatus? _bigSegmentsStatus; + private readonly bool _overrideAffected; /// /// An enum indicating the general category of the reason. @@ -130,6 +131,25 @@ public struct EvaluationReason : IJsonSerializable /// public BigSegmentsStatus? BigSegmentsStatus => _bigSegmentsStatus; + /// + /// Whether a flag override affected this evaluation, directly or transitively. + /// + /// + /// + /// 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. + /// + /// + /// In the JSON representation, the overrideAffected property appears only when this value + /// is true. + /// + /// + /// Flag overrides are currently experimental and subject to change. + /// + /// + public bool OverrideAffected => _overrideAffected; + internal EvaluationReason( EvaluationReasonKind kind, int? ruleIndex, @@ -137,7 +157,8 @@ internal EvaluationReason( string prereqKey, EvaluationErrorKind? errorKind, bool inExperiment, - BigSegmentsStatus? bigSegmentsStatus + BigSegmentsStatus? bigSegmentsStatus, + bool overrideAffected ) { _kind = kind; @@ -147,6 +168,7 @@ internal EvaluationReason( _errorKind = errorKind; _inExperiment = inExperiment; _bigSegmentsStatus = bigSegmentsStatus; + _overrideAffected = overrideAffected; } /// @@ -171,7 +193,7 @@ internal EvaluationReason( /// the unique rule ID /// a reason descriptor 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); /// /// Returns an EvaluationReason of the kind . @@ -179,7 +201,7 @@ public static EvaluationReason RuleMatchReason(int ruleIndex, string ruleId) => /// the key of the prerequisite flag /// a reason descriptor 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); /// /// Returns an EvaluationReason of the kind . @@ -187,7 +209,7 @@ public static EvaluationReason PrerequisiteFailedReason(string key) => /// /// a reason descriptor 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); /// /// Returns a copy of this EvaluationReason with a specific value added. @@ -196,7 +218,20 @@ public static EvaluationReason ErrorReason(EvaluationErrorKind errorKind) => /// a reason descriptor public EvaluationReason WithBigSegmentsStatus(BigSegmentsStatus? bigSegmentsStatus) => new EvaluationReason(_kind, _ruleIndex, _ruleId, _prerequisiteKey, _errorKind, - _inExperiment, bigSegmentsStatus); + _inExperiment, bigSegmentsStatus, _overrideAffected); + + /// + /// Returns a copy of this EvaluationReason with a specific value. + /// + /// + /// The copy keeps all other properties of this reason. Flag overrides are currently experimental + /// and subject to change. + /// + /// the new property value + /// a reason descriptor + public EvaluationReason WithOverrideAffected(bool overrideAffected) => + new EvaluationReason(_kind, _ruleIndex, _ruleId, _prerequisiteKey, _errorKind, + _inExperiment, _bigSegmentsStatus, overrideAffected); /// /// Returns a new instance with the property set to the specified @@ -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; } @@ -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; /// 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; /// public override string ToString() diff --git a/pkgs/shared/common/src/Json/LdJsonConverters.cs b/pkgs/shared/common/src/Json/LdJsonConverters.cs index 228e7b554..cca68dd46 100644 --- a/pkgs/shared/common/src/Json/LdJsonConverters.cs +++ b/pkgs/shared/common/src/Json/LdJsonConverters.cs @@ -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); @@ -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; @@ -146,6 +150,10 @@ public static EvaluationReason ReadJsonValue(ref Utf8JsonReader reader) { reason = reason.WithBigSegmentsStatus(bigSegmentsStatus); } + if (overrideAffected) + { + reason = reason.WithOverrideAffected(true); + } return reason; } @@ -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"); diff --git a/pkgs/shared/common/test/EvaluationDetailTest.cs b/pkgs/shared/common/test/EvaluationDetailTest.cs index beaf8a3a5..70973651c 100644 --- a/pkgs/shared/common/test/EvaluationDetailTest.cs +++ b/pkgs/shared/common/test/EvaluationDetailTest.cs @@ -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)" } }) { @@ -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(@"{""kind"":""OFF"",""overrideAffected"":false}"); + Assert.Equal(EvaluationReason.OffReason, reason); + } + [Fact] public void TestBigSegmentsStatusSerializationDeserialization() { @@ -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), @@ -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) ); }