diff --git a/.github/workflows/run-test-harness.yml b/.github/workflows/run-test-harness.yml index b2b1cb1..b6ddb8b 100644 --- a/.github/workflows/run-test-harness.yml +++ b/.github/workflows/run-test-harness.yml @@ -16,4 +16,4 @@ jobs: with: sdks-to-test: python sdk-github-sha: ${{github.event.pull_request.head.sha}} - sdk-capabilities: '["cloud", "edgeDB", "clientCustomData","v2Config", "allVariables", "allFeatures", "evalReason", "cloudEvalReason"]' + sdk-capabilities: '["cloud", "edgeDB", "clientCustomData","v2Config", "allVariables", "allFeatures", "evalReason", "cloudEvalReason", "eventsEvalReason"]' diff --git a/devcycle_python_sdk/bucketing-lib.release.wasm b/devcycle_python_sdk/bucketing-lib.release.wasm index 3e4843f..80994ca 100644 Binary files a/devcycle_python_sdk/bucketing-lib.release.wasm and b/devcycle_python_sdk/bucketing-lib.release.wasm differ diff --git a/devcycle_python_sdk/local_client.py b/devcycle_python_sdk/local_client.py index dbf97b5..6f687ff 100644 --- a/devcycle_python_sdk/local_client.py +++ b/devcycle_python_sdk/local_client.py @@ -135,7 +135,10 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable try: self.event_queue_manager.queue_aggregate_event( event=DevCycleEvent( - type=EventType.AggVariableDefaulted, target=key, value=1 + type=EventType.AggVariableDefaulted, + target=key, + value=1, + metaData={"evalReason": EvalReasons.DEFAULT}, ), bucketed_config=None, ) diff --git a/devcycle_python_sdk/models/variable.py b/devcycle_python_sdk/models/variable.py index 3bace9b..5928989 100644 --- a/devcycle_python_sdk/models/variable.py +++ b/devcycle_python_sdk/models/variable.py @@ -85,3 +85,19 @@ def create_default_variable( isDefaulted=True, eval=eval_reason, ) + + def get_flag_meta_data(self) -> dict: + """ + Returns metadata dictionary for OpenFeature flag resolution. + + Returns: + dict: Dictionary containing evalReasonDetails and evalReasonTargetId + if they exist, empty dict otherwise. + """ + meta_data = {} + if self.eval: + if self.eval.details: + meta_data["evalReasonDetails"] = self.eval.details + if self.eval.target_id: + meta_data["evalReasonTargetId"] = self.eval.target_id + return meta_data diff --git a/devcycle_python_sdk/open_feature_provider/provider.py b/devcycle_python_sdk/open_feature_provider/provider.py index 8de69de..aad750c 100644 --- a/devcycle_python_sdk/open_feature_provider/provider.py +++ b/devcycle_python_sdk/open_feature_provider/provider.py @@ -80,13 +80,18 @@ def _resolve( reason=Reason.DEFAULT, ) else: + # TODO: once eval enabled from cloud bucketing, eval reason won't be null unless defaulted + if variable.eval and variable.eval.reason: + reason = variable.eval.reason + elif variable.isDefaulted: + reason = Reason.DEFAULT + else: + reason = Reason.TARGETING_MATCH + return FlagResolutionDetails( value=variable.value, - reason=( - Reason.DEFAULT - if variable.isDefaulted - else Reason.TARGETING_MATCH - ), + reason=reason, + flag_metadata=variable.get_flag_meta_data(), ) except ValueError as e: # occurs if the key or default value is None diff --git a/test/openfeature_test/test_provider.py b/test/openfeature_test/test_provider.py index 0fb7a54..4277f07 100644 --- a/test/openfeature_test/test_provider.py +++ b/test/openfeature_test/test_provider.py @@ -12,6 +12,10 @@ ) from devcycle_python_sdk.models.variable import Variable, TypeEnum +from devcycle_python_sdk.models.eval_reason import ( + EvalReason, + DefaultReasonDetails, +) from devcycle_python_sdk.open_feature_provider.provider import ( DevCycleProvider, @@ -69,7 +73,9 @@ def test_resolve_details_client_returns_none(self): def test_resolve_details_client_returns_default_variable(self): self.client.variable.return_value = Variable.create_default_variable( - key="test-flag", default_value=False + key="test-flag", + default_value=False, + default_reason_detail=DefaultReasonDetails.USER_NOT_TARGETED, ) context = EvaluationContext(targeting_key="user-1234") details = self.provider._resolve("test-flag", False, context) @@ -77,6 +83,9 @@ def test_resolve_details_client_returns_default_variable(self): self.assertIsNotNone(details) self.assertEqual(details.value, False) self.assertEqual(details.reason, Reason.DEFAULT) + self.assertEqual( + details.flag_metadata["evalReasonDetails"], "User Not Targeted" + ) def test_resolve_boolean_details(self): key = "test-flag" @@ -90,6 +99,9 @@ def test_resolve_boolean_details(self): type=TypeEnum.BOOLEAN, isDefaulted=False, defaultValue=False, + eval=EvalReason( + reason="TARGETING_MATCH", details="All Users", target_id="targetId" + ), ) context = EvaluationContext(targeting_key="user-1234") @@ -98,6 +110,9 @@ def test_resolve_boolean_details(self): self.assertIsNotNone(details) self.assertEqual(details.value, variable_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users") + self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId") def test_resolve_string_details(self): key = "test-flag" @@ -111,6 +126,9 @@ def test_resolve_string_details(self): type=TypeEnum.STRING, isDefaulted=False, defaultValue=False, + eval=EvalReason( + reason="TARGETING_MATCH", details="All Users", target_id="targetId" + ), ) context = EvaluationContext(targeting_key="user-1234") @@ -120,6 +138,8 @@ def test_resolve_string_details(self): self.assertEqual(details.value, variable_value) self.assertIsInstance(details.value, str) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users") + self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId") def test_resolve_integer_details(self): key = "test-flag" @@ -133,6 +153,9 @@ def test_resolve_integer_details(self): type=TypeEnum.STRING, isDefaulted=False, defaultValue=False, + eval=EvalReason( + reason="TARGETING_MATCH", details="All Users", target_id="targetId" + ), ) context = EvaluationContext(targeting_key="user-1234") @@ -142,6 +165,8 @@ def test_resolve_integer_details(self): self.assertIsInstance(details.value, int) self.assertEqual(details.value, variable_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users") + self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId") def test_resolve_float_details(self): key = "test-flag" @@ -155,6 +180,7 @@ def test_resolve_float_details(self): type=TypeEnum.STRING, isDefaulted=False, defaultValue=False, + eval=EvalReason(reason="SPLIT", details="Rollout", target_id="targetId"), ) context = EvaluationContext(targeting_key="user-1234") @@ -163,7 +189,9 @@ def test_resolve_float_details(self): self.assertIsNotNone(details) self.assertIsInstance(details.value, float) self.assertEqual(details.value, variable_value) - self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertEqual(details.reason, Reason.SPLIT) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "Rollout") + self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId") def test_resolve_object_details_verify_default_value(self): key = "test-flag" @@ -204,6 +232,9 @@ def test_resolve_object_details(self): type=TypeEnum.STRING, isDefaulted=False, defaultValue=False, + eval=EvalReason( + reason="TARGETING_MATCH", details="Rollout", target_id="targetId" + ), ) context = EvaluationContext(targeting_key="user-1234") @@ -213,6 +244,52 @@ def test_resolve_object_details(self): self.assertIsInstance(details.value, dict) self.assertDictEqual(details.value, variable_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertIsNotNone(details.flag_metadata) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "Rollout") + self.assertEqual(details.flag_metadata["evalReasonTargetId"], "targetId") + + def test_resolve_string_details_null_eval(self): + key = "test-flag" + variable_value = "some string" + default_value = "default string" + + self.client.variable.return_value = Variable( + _id=None, + value=variable_value, + key=key, + type=TypeEnum.STRING, + isDefaulted=False, + defaultValue=False, + ) + + context = EvaluationContext(targeting_key="user-1234") + details = self.provider.resolve_string_details(key, default_value, context) + + self.assertIsNotNone(details) + self.assertEqual(details.value, variable_value) + self.assertIsInstance(details.value, str) + self.assertEqual(details.reason, Reason.TARGETING_MATCH) + + def test_default_string_details_null_eval(self): + key = "test-flag" + default_value = "default string" + + self.client.variable.return_value = Variable( + _id=None, + value=default_value, + key=key, + type=TypeEnum.STRING, + isDefaulted=True, + defaultValue=False, + ) + + context = EvaluationContext(targeting_key="user-1234") + details = self.provider.resolve_string_details(key, default_value, context) + + self.assertIsNotNone(details) + self.assertEqual(details.value, default_value) + self.assertIsInstance(details.value, str) + self.assertEqual(details.reason, Reason.DEFAULT) if __name__ == "__main__": diff --git a/test/openfeature_test/test_provider_local_sdk.py b/test/openfeature_test/test_provider_local_sdk.py index 04c3223..933f4b0 100644 --- a/test/openfeature_test/test_provider_local_sdk.py +++ b/test/openfeature_test/test_provider_local_sdk.py @@ -66,6 +66,11 @@ def test_resolve_boolean_details(self): self.assertIsNotNone(details) self.assertEqual(details.value, expected_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertIsNotNone(details.flag_metadata) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users") + self.assertEqual( + details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc" + ) @responses.activate def test_resolve_integer_details(self): @@ -82,6 +87,11 @@ def test_resolve_integer_details(self): self.assertIsNotNone(details) self.assertEqual(details.value, expected_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertIsNotNone(details.flag_metadata) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users") + self.assertEqual( + details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc" + ) @responses.activate def test_resolve_float_details(self): @@ -98,6 +108,11 @@ def test_resolve_float_details(self): self.assertIsNotNone(details) self.assertEqual(details.value, expected_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertIsNotNone(details.flag_metadata) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users") + self.assertEqual( + details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc" + ) @responses.activate def test_resolve_string_details(self): @@ -114,6 +129,11 @@ def test_resolve_string_details(self): self.assertIsNotNone(details) self.assertEqual(details.value, expected_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertIsNotNone(details.flag_metadata) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users") + self.assertEqual( + details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc" + ) @responses.activate def test_resolve_object_details(self): @@ -134,3 +154,8 @@ def test_resolve_object_details(self): self.assertIsNotNone(details) self.assertEqual(details.value, expected_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + self.assertIsNotNone(details.flag_metadata) + self.assertEqual(details.flag_metadata["evalReasonDetails"], "All Users") + self.assertEqual( + details.flag_metadata["evalReasonTargetId"], "63125321d31c601f992288bc" + ) diff --git a/update_wasm_lib.sh b/update_wasm_lib.sh index 93b0472..1200e7a 100755 --- a/update_wasm_lib.sh +++ b/update_wasm_lib.sh @@ -1,6 +1,6 @@ #!/bin/bash -BUCKETING_LIB_VERSION="1.40.2" +BUCKETING_LIB_VERSION="1.41.0" if [[ -n "$1" ]]; then BUCKETING_LIB_VERSION="$1"