Skip to content

Commit f9bd452

Browse files
committed
handle local bucketing eval reason
1 parent e524e7d commit f9bd452

File tree

5 files changed

+103
-25
lines changed

5 files changed

+103
-25
lines changed

devcycle_python_sdk/api/local_bucketing.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -323,14 +323,7 @@ def get_variable_for_user_protobuf(
323323
var_bytes = self._read_assembly_script_byte_array(variable_addr)
324324
sdk_variable = pb2.SDKVariable_PB()
325325
sdk_variable.ParseFromString(var_bytes)
326-
327-
if sdk_variable.type != pb_variable_type:
328-
# this situation should never actually happen because the WASM handles
329-
# it internally and returns a null value from the WASM function
330-
# This check is here just in case that logic changes in the future
331-
raise VariableTypeMismatchError(
332-
f"Variable returned does not match requested type: {pb_variable_type}"
333-
)
326+
334327
return pb_utils.create_variable(sdk_variable, default_value)
335328

336329
def generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:

devcycle_python_sdk/local_client.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from devcycle_python_sdk.models.bucketed_config import BucketedConfig
1818
from devcycle_python_sdk.models.eval_hook import EvalHook
1919
from devcycle_python_sdk.models.eval_hook_context import HookContext
20+
from devcycle_python_sdk.models.eval_reason import DefaultReasonDetails, EvalReason, EvalReasons
2021
from devcycle_python_sdk.models.event import DevCycleEvent, EventType
2122
from devcycle_python_sdk.models.feature import Feature
2223
from devcycle_python_sdk.models.platform_data import default_platform_data
@@ -139,7 +140,7 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
139140
logger.warning(
140141
f"DevCycle: Unable to track AggVariableDefaulted event for Variable {key}: {e}"
141142
)
142-
return Variable.create_default_variable(key, default_value)
143+
return Variable.create_default_variable(key, default_value, DefaultReasonDetails.MISSING_CONFIG)
143144

144145
context = HookContext(key, user, default_value)
145146
variable = Variable.create_default_variable(
@@ -159,22 +160,30 @@ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable
159160
)
160161
if bucketed_variable is not None:
161162
variable = bucketed_variable
163+
else:
164+
variable.eval = EvalReason(
165+
reason=EvalReasons.DEFAULT,
166+
details=DefaultReasonDetails.USER_NOT_TARGETED
167+
)
162168

163169
if before_hook_error is None:
164170
self.eval_hooks_manager.run_after(context, variable)
165171
else:
166172
raise before_hook_error
167-
except VariableTypeMismatchError:
168-
logger.debug("DevCycle: Variable type mismatch, returning default value")
169-
return variable
170-
except BeforeHookError as e:
171-
self.eval_hooks_manager.run_error(context, e)
172-
return variable
173-
except AfterHookError as e:
174-
self.eval_hooks_manager.run_error(context, e)
175-
return variable
176173
except Exception as e:
177-
logger.warning(f"DevCycle: Error retrieving variable for user: {e}")
174+
variable.eval = EvalReason(
175+
reason=EvalReasons.DEFAULT,
176+
details=DefaultReasonDetails.ERROR
177+
)
178+
179+
if isinstance(e, BeforeHookError):
180+
self.eval_hooks_manager.run_error(context, e)
181+
elif isinstance(e, AfterHookError):
182+
self.eval_hooks_manager.run_error(context, e)
183+
else:
184+
logger.warning(
185+
f"DevCycle: Error retrieving variable for user: {e}")
186+
178187
return variable
179188
finally:
180189
self.eval_hooks_manager.run_finally(context, variable)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
5+
class EvalReasons:
6+
"""Evaluation reasons constants"""
7+
DEFAULT = "DEFAULT"
8+
9+
10+
class DefaultReasonDetails:
11+
"""Default reason details constants"""
12+
MISSING_CONFIG = "Missing Config"
13+
USER_NOT_TARGETED = "User Not Targeted"
14+
TYPE_MISMATCH = "Variable Type Mismatch"
15+
ERROR = "Error"
16+
17+
18+
@dataclass(order=False)
19+
class EvalReason:
20+
reason: str
21+
details: Optional[str] = None
22+
target_id: Optional[str] = None
23+
24+
def to_json(self):
25+
return {
26+
key: getattr(self, key)
27+
for key in self.__dataclass_fields__
28+
if getattr(self, key) is not None
29+
}
30+
31+
@classmethod
32+
def from_json(cls, data: dict) -> "EvalReason":
33+
return cls(
34+
reason=data["reason"],
35+
details=data.get("details"),
36+
target_id=data.get("target_id"),
37+
)

devcycle_python_sdk/models/variable.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from dataclasses import dataclass
33
from typing import Optional, Any
44

5+
from .eval_reason import EvalReason, EvalReasons, DefaultReasonDetails
6+
57

68
class TypeEnum:
79
BOOLEAN = "Boolean"
@@ -32,16 +34,26 @@ class Variable:
3234
isDefaulted: Optional[bool] = False
3335
defaultValue: Any = None
3436
evalReason: Optional[str] = None
37+
eval: Optional[EvalReason] = None
3538

3639
def to_json(self):
37-
return {
38-
key: getattr(self, key)
39-
for key in self.__dataclass_fields__
40-
if getattr(self, key) is not None
41-
}
40+
result = {}
41+
for key in self.__dataclass_fields__:
42+
value = getattr(self, key)
43+
if value is not None:
44+
if key == "eval" and isinstance(value, EvalReason):
45+
result[key] = value.to_json()
46+
else:
47+
result[key] = value
48+
return result
4249

4350
@classmethod
4451
def from_json(cls, data: dict) -> "Variable":
52+
eval_data = data.get("eval")
53+
eval_reason = None
54+
if eval_data:
55+
eval_reason = EvalReason.from_json(eval_data)
56+
4557
return cls(
4658
_id=data["_id"],
4759
key=data["key"],
@@ -50,16 +62,25 @@ def from_json(cls, data: dict) -> "Variable":
5062
isDefaulted=data.get("isDefaulted", None),
5163
defaultValue=data.get("defaultValue"),
5264
evalReason=data.get("evalReason"),
65+
eval=eval_reason,
5366
)
5467

5568
@staticmethod
56-
def create_default_variable(key: str, default_value: Any) -> "Variable":
69+
def create_default_variable(key: str, default_value: Any, default_reason_detail: str = None) -> "Variable":
5770
var_type = determine_variable_type(default_value)
71+
if default_reason_detail is not None:
72+
eval_reason = EvalReason(
73+
reason=EvalReasons.DEFAULT,
74+
details=default_reason_detail
75+
)
76+
else:
77+
eval_reason = None
5878
return Variable(
5979
_id=None,
6080
key=key,
6181
type=var_type,
6282
value=default_value,
6383
defaultValue=default_value,
6484
isDefaulted=True,
85+
eval=eval_reason,
6586
)

devcycle_python_sdk/protobuf/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any, Optional
66

77
from devcycle_python_sdk.models.variable import TypeEnum, Variable
8+
from devcycle_python_sdk.models.eval_reason import EvalReason
89
from devcycle_python_sdk.models.user import DevCycleUser
910

1011
import devcycle_python_sdk.protobuf.variableForUserParams_pb2 as pb2
@@ -82,7 +83,20 @@ def create_dvcuser_pb(user: DevCycleUser) -> pb2.DVCUser_PB: # type: ignore
8283
)
8384

8485

86+
def create_eval_reason_from_pb(eval_reason_pb: pb2.EvalReason_PB) -> EvalReason: # type: ignore
87+
"""Convert EvalReason_PB protobuf message to EvalReason object"""
88+
return EvalReason(
89+
reason=eval_reason_pb.reason,
90+
details=eval_reason_pb.details if eval_reason_pb.details else None,
91+
target_id=eval_reason_pb.target_id if eval_reason_pb.target_id else None,
92+
)
93+
94+
8595
def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Variable: # type: ignore
96+
eval_reason_obj = None
97+
if sdk_variable.HasField('eval'):
98+
eval_reason_obj = create_eval_reason_from_pb(sdk_variable.eval)
99+
86100
if sdk_variable.type == pb2.VariableType_PB.Boolean: # type: ignore
87101
return Variable(
88102
_id=None,
@@ -91,6 +105,7 @@ def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Var
91105
type=TypeEnum.BOOLEAN,
92106
isDefaulted=False,
93107
defaultValue=default_value,
108+
eval=eval_reason_obj,
94109
)
95110

96111
elif sdk_variable.type == pb2.VariableType_PB.String: # type: ignore
@@ -101,6 +116,7 @@ def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Var
101116
type=TypeEnum.STRING,
102117
isDefaulted=False,
103118
defaultValue=default_value,
119+
eval=eval_reason_obj,
104120
)
105121

106122
elif sdk_variable.type == pb2.VariableType_PB.Number: # type: ignore
@@ -111,6 +127,7 @@ def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Var
111127
type=TypeEnum.NUMBER,
112128
isDefaulted=False,
113129
defaultValue=default_value,
130+
eval=eval_reason_obj,
114131
)
115132

116133
elif sdk_variable.type == pb2.VariableType_PB.JSON: # type: ignore
@@ -123,6 +140,7 @@ def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Var
123140
type=TypeEnum.JSON,
124141
isDefaulted=False,
125142
defaultValue=default_value,
143+
eval=eval_reason_obj,
126144
)
127145

128146
else:

0 commit comments

Comments
 (0)