From fff1c72cb9b5b97636d45816de0ea6d6c016060c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:46:31 +0000 Subject: [PATCH] feat: Populate OpenFeature flag metadata from the evaluation reason Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- README.md | 18 +++++++ .../impl/details_converter.rb | 32 +++++++++++- spec/impl/details_converter_spec.rb | 50 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7671ff8..f2a064d 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,24 @@ attributes = { context = EvaluationContext(**attributes) ``` +### Flag Metadata + +Evaluations include flag metadata for the LaunchDarkly specific parts of the evaluation result which have no OpenFeature equivalent. Each entry is absent when it does not apply to the evaluation. + +| Key | Type | Description | +|---------------------|---------|--------------------------------------------------------------------| +| `variationIndex` | integer | The index of the returned variation. Absent for default values. | +| `inExperiment` | boolean | Present, and `true`, when the evaluation was part of an experiment. | +| `ruleIndex` | integer | The index of the rule that matched. | +| `ruleId` | string | The identifier of the rule that matched. | +| `prerequisiteKey` | string | The key of the prerequisite flag that failed. | +| `bigSegmentsStatus` | string | The status of the Big Segments query made during the evaluation. | + +```ruby +details = client.fetch_boolean_details(flag_key: "my-flag", default_value: false) +in_experiment = details.flag_metadata.fetch("inExperiment", false) +``` + ## Learn more Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/ruby). diff --git a/lib/ldclient-openfeature/impl/details_converter.rb b/lib/ldclient-openfeature/impl/details_converter.rb index 55b04b9..353020b 100644 --- a/lib/ldclient-openfeature/impl/details_converter.rb +++ b/lib/ldclient-openfeature/impl/details_converter.rb @@ -7,6 +7,16 @@ module LaunchDarkly module OpenFeature module Impl class ResolutionDetailsConverter + VARIATION_INDEX_KEY = 'variationIndex' + IN_EXPERIMENT_KEY = 'inExperiment' + RULE_INDEX_KEY = 'ruleIndex' + RULE_ID_KEY = 'ruleId' + PREREQUISITE_KEY_KEY = 'prerequisiteKey' + BIG_SEGMENTS_STATUS_KEY = 'bigSegmentsStatus' + + private_constant :VARIATION_INDEX_KEY, :IN_EXPERIMENT_KEY, :RULE_INDEX_KEY, :RULE_ID_KEY, + :PREREQUISITE_KEY_KEY, :BIG_SEGMENTS_STATUS_KEY + # # @param detail [LaunchDarkly::EvaluationDetail] # @@ -35,10 +45,30 @@ def to_resolution_details(detail) error_code: openfeature_error_code, error_message: nil, reason: openfeature_reason, - variant: openfeature_variant + variant: openfeature_variant, + flag_metadata: flag_metadata(reason, is_default ? nil : variation_index) ) end + # + # @param reason [LaunchDarkly::EvaluationReason] + # @param variation_index [Integer, nil] + # + # @return [Hash] + # + private def flag_metadata(reason, variation_index) + metadata = {} + + metadata[VARIATION_INDEX_KEY] = variation_index unless variation_index.nil? + metadata[IN_EXPERIMENT_KEY] = true if reason.in_experiment + metadata[RULE_INDEX_KEY] = reason.rule_index unless reason.rule_index.nil? + metadata[RULE_ID_KEY] = reason.rule_id unless reason.rule_id.nil? + metadata[PREREQUISITE_KEY_KEY] = reason.prerequisite_key unless reason.prerequisite_key.nil? + metadata[BIG_SEGMENTS_STATUS_KEY] = reason.big_segments_status.to_s unless reason.big_segments_status.nil? + + metadata + end + # # @param kind [Symbol] # diff --git a/spec/impl/details_converter_spec.rb b/spec/impl/details_converter_spec.rb index 5576eec..c4914f8 100644 --- a/spec/impl/details_converter_spec.rb +++ b/spec/impl/details_converter_spec.rb @@ -34,4 +34,54 @@ expect(resolution_details.error_code).to eq(of_error_code) end end + + it "includes the variation index in the flag metadata" do + detail = LaunchDarkly::EvaluationDetail.new(true, 1, LaunchDarkly::EvaluationReason::fallthrough) + resolution_details = details_converter.to_resolution_details(detail) + + expect(resolution_details.flag_metadata).to eq({'variationIndex' => 1}) + end + + it "omits the variation index from the flag metadata for default values" do + detail = LaunchDarkly::EvaluationDetail.new(true, nil, LaunchDarkly::EvaluationReason::error(LaunchDarkly::EvaluationReason::ERROR_FLAG_NOT_FOUND)) + resolution_details = details_converter.to_resolution_details(detail) + + expect(resolution_details.flag_metadata).to eq({}) + end + + it "includes in experiment in the flag metadata for experiment evaluations" do + detail = LaunchDarkly::EvaluationDetail.new(true, 1, LaunchDarkly::EvaluationReason::fallthrough(true)) + resolution_details = details_converter.to_resolution_details(detail) + + expect(resolution_details.flag_metadata).to eq({'variationIndex' => 1, 'inExperiment' => true}) + end + + it "omits in experiment from the flag metadata for non-experiment evaluations" do + detail = LaunchDarkly::EvaluationDetail.new(true, 1, LaunchDarkly::EvaluationReason::fallthrough(false)) + resolution_details = details_converter.to_resolution_details(detail) + + expect(resolution_details.flag_metadata).not_to have_key('inExperiment') + end + + it "includes the rule in the flag metadata for rule matches" do + detail = LaunchDarkly::EvaluationDetail.new(true, 1, LaunchDarkly::EvaluationReason::rule_match(2, 'the-rule-id', false)) + resolution_details = details_converter.to_resolution_details(detail) + + expect(resolution_details.flag_metadata).to eq({'variationIndex' => 1, 'ruleIndex' => 2, 'ruleId' => 'the-rule-id'}) + end + + it "includes the prerequisite key in the flag metadata for failed prerequisites" do + detail = LaunchDarkly::EvaluationDetail.new(true, 1, LaunchDarkly::EvaluationReason::prerequisite_failed('the-prerequisite-key')) + resolution_details = details_converter.to_resolution_details(detail) + + expect(resolution_details.flag_metadata).to eq({'variationIndex' => 1, 'prerequisiteKey' => 'the-prerequisite-key'}) + end + + it "includes the big segments status in the flag metadata" do + reason = LaunchDarkly::EvaluationReason::fallthrough.with_big_segments_status(LaunchDarkly::BigSegmentsStatus::STALE) + detail = LaunchDarkly::EvaluationDetail.new(true, 1, reason) + resolution_details = details_converter.to_resolution_details(detail) + + expect(resolution_details.flag_metadata).to eq({'variationIndex' => 1, 'bigSegmentsStatus' => 'STALE'}) + end end