Skip to content
This repository was archived by the owner on Nov 8, 2024. It is now read-only.

Commit 0d6c4ad

Browse files
authored
Implement Assignment Logging (#10)
* implement assignment logger * fix lint errors * fix bug * make subjectAttributes property of assignment event
1 parent 5fb2989 commit 0d6c4ad

File tree

5 files changed

+81
-3
lines changed

5 files changed

+81
-3
lines changed

eppo_client/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,18 @@ def init(config: Config) -> EppoClient:
3737
config_requestor = ExperimentConfigurationRequestor(
3838
http_client=http_client, config_store=config_store
3939
)
40+
assignment_logger = config.assignment_logger
4041
global __client
4142
global __lock
4243
try:
4344
__lock.acquire_write()
4445
if __client:
4546
# if a client was already initialized, stop the background processes of the old client
4647
__client._shutdown()
47-
__client = EppoClient(config_requestor=config_requestor)
48+
__client = EppoClient(
49+
config_requestor=config_requestor,
50+
assignment_logger=assignment_logger,
51+
)
4852
return __client
4953
finally:
5054
__lock.release_write()

eppo_client/assignment_logger.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from typing import Dict
2+
3+
4+
class AssignmentLogger:
5+
def log_assignment(self, assignment_event: Dict):
6+
pass

eppo_client/client.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import hashlib
2+
import datetime
3+
import logging
24
from typing import List, Optional
5+
from eppo_client.assignment_logger import AssignmentLogger
36
from eppo_client.configuration_requestor import (
47
ExperimentConfigurationDto,
58
ExperimentConfigurationRequestor,
@@ -10,10 +13,17 @@
1013
from eppo_client.shard import get_shard, is_in_shard_range
1114
from eppo_client.validation import validate_not_blank
1215

16+
logger = logging.getLogger(__name__)
17+
1318

1419
class EppoClient:
15-
def __init__(self, config_requestor: ExperimentConfigurationRequestor):
20+
def __init__(
21+
self,
22+
config_requestor: ExperimentConfigurationRequestor,
23+
assignment_logger: AssignmentLogger = AssignmentLogger(),
24+
):
1625
self.__config_requestor = config_requestor
26+
self.__assignment_logger = assignment_logger
1727
self.__poller = Poller(
1828
interval_millis=POLL_INTERVAL_MILLIS,
1929
jitter_millis=POLL_JITTER_MILLIS,
@@ -53,14 +63,26 @@ def get_assignment(
5363
"assignment-{}-{}".format(subject_key, experiment_key),
5464
experiment_config.subject_shards,
5565
)
56-
return next(
66+
assigned_variation = next(
5767
(
5868
variation.name
5969
for variation in experiment_config.variations
6070
if is_in_shard_range(shard, variation.shard_range)
6171
),
6272
None,
6373
)
74+
assignment_event = {
75+
"experiment": experiment_key,
76+
"variation": assigned_variation,
77+
"subject": subject_key,
78+
"timestamp": datetime.datetime.utcnow().isoformat(),
79+
"subjectAttributes": subject_attributes,
80+
}
81+
try:
82+
self.__assignment_logger.log_assignment(assignment_event)
83+
except Exception as e:
84+
logger.error("[Eppo SDK] Error logging assignment event: " + str(e))
85+
return assigned_variation
6486

6587
def _subject_attributes_satisfy_rules(
6688
self, subject_attributes: dict, rules: List[Rule]

eppo_client/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from eppo_client.assignment_logger import AssignmentLogger
12
from eppo_client.base_model import SdkBaseModel
23

34
from eppo_client.validation import validate_not_blank
@@ -6,6 +7,11 @@
67
class Config(SdkBaseModel):
78
api_key: str
89
base_url: str = "https://eppo.cloud/api"
10+
assignment_logger: AssignmentLogger = AssignmentLogger()
911

1012
def _validate(self):
1113
validate_not_blank("api_key", self.api_key)
14+
15+
class Config:
16+
# needed for the AssignmentLogger class which is not of type SdkBaseModel
17+
arbitrary_types_allowed = True

test/client_test.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,46 @@ def test_assign_subject_not_in_sample(mock_config_requestor):
8282
assert client.get_assignment("user-1", "experiment-key-1") is None
8383

8484

85+
@patch("eppo_client.assignment_logger.AssignmentLogger")
86+
@patch("eppo_client.configuration_requestor.ExperimentConfigurationRequestor")
87+
def test_log_assignment(mock_config_requestor, mock_logger):
88+
mock_config_requestor.get_configuration.return_value = ExperimentConfigurationDto(
89+
subjectShards=10000,
90+
percentExposure=100,
91+
enabled=True,
92+
variations=[
93+
VariationDto(name="control", shardRange=ShardRange(start=0, end=10000))
94+
],
95+
name="recommendation_algo",
96+
overrides=dict(),
97+
)
98+
client = EppoClient(
99+
config_requestor=mock_config_requestor, assignment_logger=mock_logger
100+
)
101+
assert client.get_assignment("user-1", "experiment-key-1") == "control"
102+
assert mock_logger.log_assignment.call_count == 1
103+
104+
105+
@patch("eppo_client.assignment_logger.AssignmentLogger")
106+
@patch("eppo_client.configuration_requestor.ExperimentConfigurationRequestor")
107+
def test_get_assignment_handles_logging_exception(mock_config_requestor, mock_logger):
108+
mock_config_requestor.get_configuration.return_value = ExperimentConfigurationDto(
109+
subjectShards=10000,
110+
percentExposure=100,
111+
enabled=True,
112+
variations=[
113+
VariationDto(name="control", shardRange=ShardRange(start=0, end=10000))
114+
],
115+
name="recommendation_algo",
116+
overrides=dict(),
117+
)
118+
mock_logger.log_assignment.side_effect = ValueError("logging error")
119+
client = EppoClient(
120+
config_requestor=mock_config_requestor, assignment_logger=mock_logger
121+
)
122+
assert client.get_assignment("user-1", "experiment-key-1") == "control"
123+
124+
85125
@patch("eppo_client.configuration_requestor.ExperimentConfigurationRequestor")
86126
def test_assign_subject_with_with_attributes_and_rules(mock_config_requestor):
87127
matches_email_condition = Condition(

0 commit comments

Comments
 (0)