diff --git a/libs/common/include/launchdarkly/data/evaluation_reason.hpp b/libs/common/include/launchdarkly/data/evaluation_reason.hpp index 75a6fa282..28a608c14 100644 --- a/libs/common/include/launchdarkly/data/evaluation_reason.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_reason.hpp @@ -55,6 +55,27 @@ class EvaluationReason { friend std::ostream& operator<<(std::ostream& out, ErrorKind const& kind); + /** + * Do not change these values. They must remain stable for the C API. + */ + enum class BigSegmentsStatus { + // The evaluation did not query any Big Segments. + kNone = 0, + // The query was successful and the segment state is up to date. + kHealthy = 1, + // The query was successful, but the segment state may not be up to + // date. + kStale = 2, + // Big Segments could not be queried because the SDK configuration did + // not include a Big Segment store. + kNotConfigured = 3, + // The query failed, for instance due to a database error. + kStoreError = 4, + }; + + friend std::ostream& operator<<(std::ostream& out, + BigSegmentsStatus const& status); + /** * @return The general category of the reason. */ @@ -66,6 +87,21 @@ class EvaluationReason { */ [[nodiscard]] std::optional ErrorKind() const; + /** + * The validity of the Big Segment information used in this evaluation, or + * BigSegmentsStatus::kNone if the evaluation did not query any Big + * Segments. + */ + [[nodiscard]] enum BigSegmentsStatus BigSegmentsStatus() const; + + /** + * Deprecated; use BigSegmentsStatus() instead. Returns the string passed + * to the deprecated string-typed constructor, or std::nullopt otherwise. + */ + [[deprecated("use BigSegmentsStatus()")]] [[nodiscard]] std::optional< + std::string> + BigSegmentStatus() const; + /** * The index of the matched rule (0 for the first), if the kind was * `"RULE_MATCH"`. @@ -93,29 +129,25 @@ class EvaluationReason { */ [[nodiscard]] bool InExperiment() const; - /** - * Describes the validity of Big Segment information, if and only if the - * flag evaluation required querying at least one Big Segment. - * - * - `"HEALTHY"`: The Big Segment query involved in the flag evaluation was - * successful, and the segment state is considered up to date. - * - `"STALE"`: The Big Segment query involved in the flag evaluation was - * successful, but the segment state may not be up to date - * - `"NOT_CONFIGURED"`: Big Segments could not be queried for the flag - * evaluation because the SDK configuration did not include a Big Segment - * store. - * - `"STORE_ERROR"`: The Big Segment query involved in the flag evaluation - * failed, for instance due to a database error. - */ - [[nodiscard]] std::optional BigSegmentStatus() const; - EvaluationReason(enum Kind kind, std::optional error_kind, std::optional rule_index, std::optional rule_id, std::optional prerequisite_key, bool in_experiment, - std::optional big_segment_status); + enum BigSegmentsStatus big_segments_status); + + /** + * Deprecated; use the overload taking @ref BigSegmentsStatus instead. + */ + [[deprecated("use the BigSegmentsStatus overload")]] EvaluationReason( + enum Kind kind, + std::optional error_kind, + std::optional rule_index, + std::optional rule_id, + std::optional prerequisite_key, + bool in_experiment, + std::optional big_segment_status); explicit EvaluationReason(enum ErrorKind error_kind); @@ -158,6 +190,9 @@ class EvaluationReason { friend std::ostream& operator<<(std::ostream& out, EvaluationReason const& reason); + friend bool operator==(EvaluationReason const& lhs, + EvaluationReason const& rhs); + private: enum Kind kind_; std::optional error_kind_; @@ -165,6 +200,7 @@ class EvaluationReason { std::optional rule_id_; std::optional prerequisite_key_; bool in_experiment_; + enum BigSegmentsStatus big_segments_status_; std::optional big_segment_status_; }; diff --git a/libs/common/src/data/evaluation_reason.cpp b/libs/common/src/data/evaluation_reason.cpp index 7fb433007..a60ab8905 100644 --- a/libs/common/src/data/evaluation_reason.cpp +++ b/libs/common/src/data/evaluation_reason.cpp @@ -27,10 +27,32 @@ bool EvaluationReason::InExperiment() const { return in_experiment_; } +enum EvaluationReason::BigSegmentsStatus +EvaluationReason::BigSegmentsStatus() const { + return big_segments_status_; +} + std::optional EvaluationReason::BigSegmentStatus() const { return big_segment_status_; } +EvaluationReason::EvaluationReason( + enum Kind kind, + std::optional error_kind, + std::optional rule_index, + std::optional rule_id, + std::optional prerequisite_key, + bool in_experiment, + enum BigSegmentsStatus big_segments_status) + : kind_(kind), + error_kind_(error_kind), + rule_index_(rule_index), + rule_id_(std::move(rule_id)), + prerequisite_key_(std::move(prerequisite_key)), + in_experiment_(in_experiment), + big_segments_status_(big_segments_status), + big_segment_status_(std::nullopt) {} + EvaluationReason::EvaluationReason( enum Kind kind, std::optional error_kind, @@ -45,44 +67,69 @@ EvaluationReason::EvaluationReason( rule_id_(std::move(rule_id)), prerequisite_key_(std::move(prerequisite_key)), in_experiment_(in_experiment), + big_segments_status_(BigSegmentsStatus::kNone), big_segment_status_(std::move(big_segment_status)) {} EvaluationReason::EvaluationReason(enum ErrorKind error_kind) : EvaluationReason(Kind::kError, error_kind, - std::nullopt, - std::nullopt, - std::nullopt, - false, - std::nullopt) {} + /* rule_index= */ std::nullopt, + /* rule_id= */ std::nullopt, + /* prerequisite_key= */ std::nullopt, + /* in_experiment= */ false, + BigSegmentsStatus::kNone) {} EvaluationReason EvaluationReason::Off() { - return {Kind::kOff, std::nullopt, std::nullopt, std::nullopt, - std::nullopt, false, std::nullopt}; + return {Kind::kOff, + /* error_kind= */ std::nullopt, + /* rule_index= */ std::nullopt, + /* rule_id= */ std::nullopt, + /* prerequisite_key= */ std::nullopt, + /* in_experiment= */ false, + BigSegmentsStatus::kNone}; } EvaluationReason EvaluationReason::PrerequisiteFailed( std::string prerequisite_key) { - return { - Kind::kPrerequisiteFailed, std::nullopt, std::nullopt, std::nullopt, - std::move(prerequisite_key), false, std::nullopt}; + return {Kind::kPrerequisiteFailed, + /* error_kind= */ std::nullopt, + /* rule_index= */ std::nullopt, + /* rule_id= */ std::nullopt, + std::move(prerequisite_key), + /* in_experiment= */ false, + BigSegmentsStatus::kNone}; } EvaluationReason EvaluationReason::TargetMatch() { - return {Kind::kTargetMatch, std::nullopt, std::nullopt, std::nullopt, - std::nullopt, false, std::nullopt}; + return {Kind::kTargetMatch, + /* error_kind= */ std::nullopt, + /* rule_index= */ std::nullopt, + /* rule_id= */ std::nullopt, + /* prerequisite_key= */ std::nullopt, + /* in_experiment= */ false, + BigSegmentsStatus::kNone}; } EvaluationReason EvaluationReason::Fallthrough(bool in_experiment) { - return {Kind::kFallthrough, std::nullopt, std::nullopt, std::nullopt, - std::nullopt, in_experiment, std::nullopt}; + return {Kind::kFallthrough, + /* error_kind= */ std::nullopt, + /* rule_index= */ std::nullopt, + /* rule_id= */ std::nullopt, + /* prerequisite_key= */ std::nullopt, + in_experiment, + BigSegmentsStatus::kNone}; } EvaluationReason EvaluationReason::RuleMatch(std::size_t rule_index, std::optional rule_id, bool in_experiment) { - return {Kind::kRuleMatch, std::nullopt, rule_index, std::move(rule_id), - std::nullopt, in_experiment, std::nullopt}; + return {Kind::kRuleMatch, + /* error_kind= */ std::nullopt, + rule_index, + std::move(rule_id), + /* prerequisite_key= */ std::nullopt, + in_experiment, + BigSegmentsStatus::kNone}; } EvaluationReason EvaluationReason::MalformedFlag() { @@ -105,8 +152,9 @@ std::ostream& operator<<(std::ostream& out, EvaluationReason const& reason) { out << " prerequisiteKey: " << reason.prerequisite_key_.value(); } out << " inExperiment: " << reason.in_experiment_; - if (reason.big_segment_status_.has_value()) { - out << " bigSegmentStatus: " << reason.big_segment_status_.value(); + if (reason.big_segments_status_ != + EvaluationReason::BigSegmentsStatus::kNone) { + out << " bigSegmentsStatus: " << reason.big_segments_status_; } out << "}"; return out; @@ -115,7 +163,8 @@ std::ostream& operator<<(std::ostream& out, EvaluationReason const& reason) { bool operator==(EvaluationReason const& lhs, EvaluationReason const& rhs) { return lhs.Kind() == rhs.Kind() && lhs.ErrorKind() == rhs.ErrorKind() && lhs.InExperiment() == rhs.InExperiment() && - lhs.BigSegmentStatus() == rhs.BigSegmentStatus() && + lhs.BigSegmentsStatus() == rhs.BigSegmentsStatus() && + lhs.big_segment_status_ == rhs.big_segment_status_ && lhs.PrerequisiteKey() == rhs.PrerequisiteKey() && lhs.RuleId() == rhs.RuleId() && lhs.RuleIndex() == rhs.RuleIndex(); } @@ -149,6 +198,29 @@ std::ostream& operator<<(std::ostream& out, return out; } +std::ostream& operator<<( + std::ostream& out, + enum EvaluationReason::BigSegmentsStatus const& status) { + switch (status) { + case EvaluationReason::BigSegmentsStatus::kNone: + out << "NONE"; + break; + case EvaluationReason::BigSegmentsStatus::kHealthy: + out << "HEALTHY"; + break; + case EvaluationReason::BigSegmentsStatus::kStale: + out << "STALE"; + break; + case EvaluationReason::BigSegmentsStatus::kNotConfigured: + out << "NOT_CONFIGURED"; + break; + case EvaluationReason::BigSegmentsStatus::kStoreError: + out << "STORE_ERROR"; + break; + } + return out; +} + std::ostream& operator<<(std::ostream& out, enum EvaluationReason::ErrorKind const& kind) { switch (kind) { diff --git a/libs/common/tests/bindings/evaluation_detail_test.cpp b/libs/common/tests/bindings/evaluation_detail_test.cpp index 74dc59438..af15a40bb 100644 --- a/libs/common/tests/bindings/evaluation_detail_test.cpp +++ b/libs/common/tests/bindings/evaluation_detail_test.cpp @@ -22,8 +22,12 @@ TEST(EvaluationDetailBindings, EvaluationReasonError) { TEST(EvaluationDetailBindings, EvaluationReasonFallthrough) { auto reason = EvaluationReason(EvaluationReason::Kind::kFallthrough, - std::nullopt, std::nullopt, std::nullopt, - std::nullopt, true, std::nullopt); + /* error_kind= */ std::nullopt, + /* rule_index= */ std::nullopt, + /* rule_id= */ std::nullopt, + /* prerequisite_key= */ std::nullopt, + /* in_experiment= */ true, + EvaluationReason::BigSegmentsStatus::kNone); auto ld_reason = reinterpret_cast(&reason); ASSERT_EQ(LDEvalReason_Kind(ld_reason), LD_EVALREASON_FALLTHROUGH); @@ -52,9 +56,13 @@ TEST(EvaluationDetailBindings, EvaluationDetailError) { TEST(EvaluationDetailBindings, EvaluationDetailSuccess) { auto detail = CEvaluationDetail(EvaluationDetail( true, 42, - EvaluationReason(EvaluationReason::Kind::kFallthrough, std::nullopt, - std::nullopt, std::nullopt, std::nullopt, true, - std::nullopt))); + EvaluationReason(EvaluationReason::Kind::kFallthrough, + /* error_kind= */ std::nullopt, + /* rule_index= */ std::nullopt, + /* rule_id= */ std::nullopt, + /* prerequisite_key= */ std::nullopt, + /* in_experiment= */ true, + EvaluationReason::BigSegmentsStatus::kNone))); auto ld_detail = reinterpret_cast(&detail); diff --git a/libs/internal/src/serialization/json_evaluation_reason.cpp b/libs/internal/src/serialization/json_evaluation_reason.cpp index e8173e725..495a55554 100644 --- a/libs/internal/src/serialization/json_evaluation_reason.cpp +++ b/libs/internal/src/serialization/json_evaluation_reason.cpp @@ -125,6 +125,58 @@ void tag_invoke(boost::json::value_from_tag const& unused, } } +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (!json_value.is_string()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto const& str = json_value.as_string(); + if (str == "HEALTHY") { + return EvaluationReason::BigSegmentsStatus::kHealthy; + } + if (str == "STALE") { + return EvaluationReason::BigSegmentsStatus::kStale; + } + if (str == "NOT_CONFIGURED") { + return EvaluationReason::BigSegmentsStatus::kNotConfigured; + } + if (str == "STORE_ERROR") { + return EvaluationReason::BigSegmentsStatus::kStoreError; + } + return tl::make_unexpected(JsonError::kSchemaFailure); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + enum EvaluationReason::BigSegmentsStatus const& status) { + boost::ignore_unused(unused); + + auto& str = json_value.emplace_string(); + switch (status) { + case EvaluationReason::BigSegmentsStatus::kNone: + // kNone is the "no status" sentinel and shouldn't reach the wire; + // callers guard. If it ever does, emit an empty string. + break; + case EvaluationReason::BigSegmentsStatus::kHealthy: + str = "HEALTHY"; + break; + case EvaluationReason::BigSegmentsStatus::kStale: + str = "STALE"; + break; + case EvaluationReason::BigSegmentsStatus::kNotConfigured: + str = "NOT_CONFIGURED"; + break; + case EvaluationReason::BigSegmentsStatus::kStoreError: + str = "STORE_ERROR"; + break; + } +} + tl::expected tag_invoke( boost::json::value_to_tag> const& unused, @@ -172,9 +224,17 @@ tl::expected tag_invoke( auto in_experiment = ValueOrDefault(in_experiment_iter, json_obj.end(), false); - auto* big_segment_status_iter = json_obj.find("bigSegmentStatus"); - auto big_segment_status = - ValueAsOpt(big_segment_status_iter, json_obj.end()); + auto* big_segments_status_iter = json_obj.find("bigSegmentsStatus"); + auto big_segments_status = EvaluationReason::BigSegmentsStatus::kNone; + if (big_segments_status_iter != json_obj.end()) { + auto parsed = boost::json::value_to>( + big_segments_status_iter->value()); + if (!parsed) { + return tl::make_unexpected(parsed.error()); + } + big_segments_status = parsed.value(); + } return EvaluationReason{*kind, error_kind, @@ -182,7 +242,7 @@ tl::expected tag_invoke( rule_id, prerequisite_key, in_experiment, - big_segment_status}; + big_segments_status}; } return tl::unexpected(JsonError::kSchemaFailure); } @@ -197,8 +257,10 @@ void tag_invoke(boost::json::value_from_tag const& unused, if (auto error_kind = reason.ErrorKind()) { obj.emplace("errorKind", boost::json::value_from(*error_kind)); } - if (auto big_segment_status = reason.BigSegmentStatus()) { - obj.emplace("bigSegmentStatus", *big_segment_status); + auto big_segments_status = reason.BigSegmentsStatus(); + if (big_segments_status != EvaluationReason::BigSegmentsStatus::kNone) { + obj.emplace("bigSegmentsStatus", + boost::json::value_from(big_segments_status)); } if (auto rule_id = reason.RuleId()) { obj.emplace("ruleId", *rule_id); diff --git a/libs/internal/tests/evaluation_reason_test.cpp b/libs/internal/tests/evaluation_reason_test.cpp index 859b7d791..680223ef1 100644 --- a/libs/internal/tests/evaluation_reason_test.cpp +++ b/libs/internal/tests/evaluation_reason_test.cpp @@ -18,7 +18,7 @@ TEST(EvaluationReasonsTests, FromJsonAllFields) { "\"ruleId\":\"RULE_ID\"," "\"prerequisiteKey\":\"PREREQ_KEY\"," "\"inExperiment\":true," - "\"bigSegmentStatus\":\"STORE_ERROR\"" + "\"bigSegmentsStatus\":\"STORE_ERROR\"" "}")); EXPECT_EQ(EvaluationReason::Kind::kOff, reason.value().Kind()); @@ -27,7 +27,8 @@ TEST(EvaluationReasonsTests, FromJsonAllFields) { EXPECT_EQ(12, reason.value().RuleIndex()); EXPECT_EQ("RULE_ID", reason.value().RuleId()); EXPECT_EQ("PREREQ_KEY", reason.value().PrerequisiteKey()); - EXPECT_EQ("STORE_ERROR", reason.value().BigSegmentStatus()); + EXPECT_EQ(EvaluationReason::BigSegmentsStatus::kStoreError, + reason.value().BigSegmentsStatus()); EXPECT_TRUE(reason.value().InExperiment()); } @@ -43,7 +44,8 @@ TEST(EvaluationReasonsTests, FromMinimalJson) { EXPECT_EQ(std::nullopt, reason.value().RuleIndex()); EXPECT_EQ(std::nullopt, reason.value().RuleId()); EXPECT_EQ(std::nullopt, reason.value().PrerequisiteKey()); - EXPECT_EQ(std::nullopt, reason.value().BigSegmentStatus()); + EXPECT_EQ(EvaluationReason::BigSegmentsStatus::kNone, + reason.value().BigSegmentsStatus()); EXPECT_FALSE(reason.value().InExperiment()); } diff --git a/libs/internal/tests/evaluation_result_test.cpp b/libs/internal/tests/evaluation_result_test.cpp index 48956a3af..3d6727295 100644 --- a/libs/internal/tests/evaluation_result_test.cpp +++ b/libs/internal/tests/evaluation_result_test.cpp @@ -32,7 +32,7 @@ TEST(EvaluationResultTests, FromJsonAllFields) { "\"ruleId\":\"RULE_ID\"," "\"prerequisiteKey\":\"PREREQ_KEY\"," "\"inExperiment\":true," - "\"bigSegmentStatus\":\"STORE_ERROR\"" + "\"bigSegmentsStatus\":\"STORE_ERROR\"" "}" "}")); @@ -56,14 +56,16 @@ TEST(EvaluationResultTests, FromJsonAllFields) { EXPECT_EQ(12, val->Detail().Reason()->get().RuleIndex()); EXPECT_EQ("RULE_ID", val->Detail().Reason()->get().RuleId()); EXPECT_EQ("PREREQ_KEY", val->Detail().Reason()->get().PrerequisiteKey()); - EXPECT_EQ("STORE_ERROR", val->Detail().Reason()->get().BigSegmentStatus()); + EXPECT_EQ(EvaluationReason::BigSegmentsStatus::kStoreError, + val->Detail().Reason()->get().BigSegmentsStatus()); EXPECT_TRUE(val->Detail().Reason()->get().InExperiment()); } TEST(EvaluationResultTests, ToJsonAllFields) { EvaluationReason reason(EvaluationReason::Kind::kOff, EvaluationReason::ErrorKind::kMalformedFlag, 12, - "RULE_ID", "PREREQ_KEY", true, "STORE_ERROR"); + "RULE_ID", "PREREQ_KEY", true, + EvaluationReason::BigSegmentsStatus::kStoreError); launchdarkly::EvaluationDetailInternal detail( Value(std::map{{"item", "a"}}), 84, reason); EvaluationResult result(12, 24, true, true, @@ -79,7 +81,7 @@ TEST(EvaluationResultTests, ToJsonAllFields) { "{\"version\":12,\"flagVersion\":24,\"trackEvents\":true," "\"trackReason\":true,\"debugEventsUntilDate\":1680555761,\"value\":{" "\"item\":\"a\"},\"variationIndex\":84,\"reason\":{\"kind\":\"OFF\"," - "\"errorKind\":\"MALFORMED_FLAG\",\"bigSegmentStatus\":\"STORE_ERROR\"," + "\"errorKind\":\"MALFORMED_FLAG\",\"bigSegmentsStatus\":\"STORE_ERROR\"," "\"ruleId\":\"RULE_ID\",\"ruleIndex\":12,\"inExperiment\":true," "\"prerequisiteKey\":\"PREREQ_KEY\"}}", res); diff --git a/libs/internal/tests/event_summarizer_test.cpp b/libs/internal/tests/event_summarizer_test.cpp index 2c685254f..d6791d8cf 100644 --- a/libs/internal/tests/event_summarizer_test.cpp +++ b/libs/internal/tests/event_summarizer_test.cpp @@ -124,8 +124,13 @@ static FeatureEventParams FeatureEventFromParams(EvaluationParams params, params.feature_version, params.feature_variation, launchdarkly::EvaluationReason( - launchdarkly::EvaluationReason::Kind::kFallthrough, std::nullopt, - std::nullopt, std::nullopt, std::nullopt, false, std::nullopt), + launchdarkly::EvaluationReason::Kind::kFallthrough, + /* error_kind= */ std::nullopt, + /* rule_index= */ std::nullopt, + /* rule_id= */ std::nullopt, + /* prerequisite_key= */ std::nullopt, + /* in_experiment= */ false, + launchdarkly::EvaluationReason::BigSegmentsStatus::kNone), false, std::nullopt, }; @@ -356,8 +361,11 @@ TEST(SummarizerTests, MissingFlagCreatesCounterUsingDefaultValue) { EvaluationReason(EvaluationReason::Kind::kError, EvaluationReason::ErrorKind::kFlagNotFound, - std::nullopt, std::nullopt, std::nullopt, false, - std::nullopt), + /* rule_index= */ std::nullopt, + /* rule_id= */ std::nullopt, + /* prerequisite_key= */ std::nullopt, + /* in_experiment= */ false, + EvaluationReason::BigSegmentsStatus::kNone), false, std::nullopt,