Skip to content
Open
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@ attributes = {
context = EvaluationContext(null, 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. |

```python
details = client.get_boolean_details("my-flag", False, context)
in_experiment = details.flag_metadata.get("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/python).
Expand Down
48 changes: 43 additions & 5 deletions ld_openfeature/impl/details_converter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
from typing import Optional
from typing import Any, Dict, Mapping, Optional, Union

from ldclient.evaluation import EvaluationDetail
from openfeature.exception import ErrorCode
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.flag_evaluation import (
FlagMetadata,
FlagResolutionDetails,
Reason,
)

_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'


class ResolutionDetailsConverter:
Expand Down Expand Up @@ -30,10 +41,37 @@ def to_resolution_details(self, result: EvaluationDetail) -> FlagResolutionDetai
error_code=openfeature_error_code,
error_message=None,
reason=openfeature_reason,
variant=openfeature_variant
# flag_metadata = FlagMetadata = field(default_factory=dict)
variant=openfeature_variant,
flag_metadata=self.__to_flag_metadata(reason, variation_index if not is_default else None),
)
pass

@staticmethod
def __to_flag_metadata(reason: Mapping[str, Any], variation_index: Optional[int]) -> FlagMetadata:
metadata: Dict[str, Union[bool, int, float, str]] = {}

if variation_index is not None:
metadata[_VARIATION_INDEX_KEY] = variation_index

if reason.get('inExperiment') is True:
metadata[_IN_EXPERIMENT_KEY] = True

rule_index = reason.get('ruleIndex')
if isinstance(rule_index, int):
metadata[_RULE_INDEX_KEY] = rule_index

rule_id = reason.get('ruleId')
if isinstance(rule_id, str):
metadata[_RULE_ID_KEY] = rule_id

prerequisite_key = reason.get('prerequisiteKey')
if isinstance(prerequisite_key, str):
metadata[_PREREQUISITE_KEY_KEY] = prerequisite_key

big_segments_status = reason.get('bigSegmentsStatus')
if isinstance(big_segments_status, str):
metadata[_BIG_SEGMENTS_STATUS_KEY] = big_segments_status

return metadata

@staticmethod
def __kind_to_reason(kind: str) -> str:
Expand Down
42 changes: 42 additions & 0 deletions tests/impl/test_details_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,45 @@ def test_ld_to_openfeature_error_kind_mappings(error_kind: Optional[str], error_
resolution_details = details_converter.to_resolution_details(detail)
assert resolution_details.reason == Reason.ERROR
assert resolution_details.error_code == error_code


def test_flag_metadata_includes_the_variation_index(details_converter: ResolutionDetailsConverter):
detail = EvaluationDetail(True, 1, {'kind': 'FALLTHROUGH'})
resolution_details = details_converter.to_resolution_details(detail)
assert resolution_details.flag_metadata == {'variationIndex': 1}


def test_flag_metadata_omits_the_variation_index_for_default_values(details_converter: ResolutionDetailsConverter):
detail = EvaluationDetail(True, None, {'kind': 'ERROR', 'errorKind': 'FLAG_NOT_FOUND'})
resolution_details = details_converter.to_resolution_details(detail)
assert resolution_details.flag_metadata == {}


def test_flag_metadata_includes_in_experiment_for_experiment_evaluations(details_converter: ResolutionDetailsConverter):
detail = EvaluationDetail(True, 1, {'kind': 'FALLTHROUGH', 'inExperiment': True})
resolution_details = details_converter.to_resolution_details(detail)
assert resolution_details.flag_metadata == {'variationIndex': 1, 'inExperiment': True}


def test_flag_metadata_omits_in_experiment_for_non_experiment_evaluations(details_converter: ResolutionDetailsConverter):
detail = EvaluationDetail(True, 1, {'kind': 'FALLTHROUGH', 'inExperiment': False})
resolution_details = details_converter.to_resolution_details(detail)
assert 'inExperiment' not in resolution_details.flag_metadata


def test_flag_metadata_includes_the_rule_for_rule_matches(details_converter: ResolutionDetailsConverter):
detail = EvaluationDetail(True, 1, {'kind': 'RULE_MATCH', 'ruleIndex': 2, 'ruleId': 'the-rule-id'})
resolution_details = details_converter.to_resolution_details(detail)
assert resolution_details.flag_metadata == {'variationIndex': 1, 'ruleIndex': 2, 'ruleId': 'the-rule-id'}


def test_flag_metadata_includes_the_prerequisite_key(details_converter: ResolutionDetailsConverter):
detail = EvaluationDetail(True, 1, {'kind': 'PREREQUISITE_FAILED', 'prerequisiteKey': 'the-prerequisite-key'})
resolution_details = details_converter.to_resolution_details(detail)
assert resolution_details.flag_metadata == {'variationIndex': 1, 'prerequisiteKey': 'the-prerequisite-key'}


def test_flag_metadata_includes_the_big_segments_status(details_converter: ResolutionDetailsConverter):
detail = EvaluationDetail(True, 1, {'kind': 'FALLTHROUGH', 'bigSegmentsStatus': 'STALE'})
resolution_details = details_converter.to_resolution_details(detail)
assert resolution_details.flag_metadata == {'variationIndex': 1, 'bigSegmentsStatus': 'STALE'}
Loading