Skip to content

Commit 46598e5

Browse files
authored
Merge pull request #84 from reddit/use_RustDecider_choose_in_get_variant_for_identifier
Use `RustDecider#choose()` in `for_identifier*()` api
2 parents b087fc5 + bad81fc commit 46598e5

File tree

2 files changed

+61
-116
lines changed

2 files changed

+61
-116
lines changed

reddit_decider/__init__.py

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,7 @@ def _format_decision(self, decision_dict: Dict[str, str]) -> Dict[str, Any]:
212212

213213
return out
214214

215-
def _send_expose(
216-
self, event: str, exposure_fields: dict, overwrite_identifier: bool = False
217-
) -> None:
215+
def _send_expose(self, event: str, exposure_fields: dict) -> None:
218216
event_fields = deepcopy(exposure_fields)
219217
try:
220218
(
@@ -245,8 +243,7 @@ def _send_expose(
245243
owner=owner,
246244
)
247245

248-
if overwrite_identifier:
249-
event_fields = {**event_fields, **{bucket_val: bucketing_value}}
246+
event_fields = {**event_fields, **{bucket_val: bucketing_value}}
250247

251248
self._event_logger.log(
252249
experiment=experiment,
@@ -258,9 +255,7 @@ def _send_expose(
258255
)
259256
return
260257

261-
def _send_expose_if_holdout(
262-
self, event: str, exposure_fields: dict, overwrite_identifier: bool = False
263-
) -> None:
258+
def _send_expose_if_holdout(self, event: str, exposure_fields: dict) -> None:
264259
event_fields = deepcopy(exposure_fields)
265260
try:
266261
(
@@ -296,8 +291,7 @@ def _send_expose_if_holdout(
296291
owner=owner,
297292
)
298293

299-
if overwrite_identifier:
300-
event_fields = {**event_fields, **{bucket_val: bucketing_value}}
294+
event_fields = {**event_fields, **{bucket_val: bucketing_value}}
301295

302296
self._event_logger.log(
303297
experiment=experiment,
@@ -459,6 +453,14 @@ def get_variant_for_identifier(
459453
) -> Optional[str]:
460454
"""Return a bucketing variant, if any, with auto-exposure for a given :code:`identifier`.
461455
456+
Note: If the experiment's :code:`bucket_val` (e.g. "user_id", "device_id", "canonical_url")
457+
does not match the :code:`identifier_type` param,
458+
the :code:`identifier` will be ignored and not used to bucket (:code:`{identifier_type: identifier}` is
459+
added to internal :code:`DeciderContext` instance, but doesn't act like a bucketing override).
460+
461+
If the :code:`bucket_val` field exists on the :code:`DeciderContext` instance,
462+
that field will be used to bucket, since it corresponds to the experiment's config.
463+
462464
Since calling :code:`get_variant_for_identifier()` will fire an exposure event, it
463465
is best to call it when you are sure the user will be exposed to the experiment.
464466
@@ -467,8 +469,11 @@ def get_variant_for_identifier(
467469
:param identifier: an arbitary string used to bucket the experiment by
468470
being set on :code:`DeciderContext`'s :code:`identifier_type` field.
469471
470-
:param identifier_type: Sets :code:`{identifier_type: identifier}` on DeciderContext and
471-
should match an experiment's :code:`bucket_val` to get a variant.
472+
:param identifier_type: Sets :code:`{identifier_type: identifier}` on :code:`DeciderContext`.
473+
The experiment's :code:`bucket_val` will be looked up in :code:`DeciderContext` and be used to bucket.
474+
If the experiment's :code:`bucket_val` field does not match :code:`identifier_type` param,
475+
:code:`identifier` will be ignored, and the field corresponding :code:`bucket_val` will be looked up
476+
from :code:`DeciderContext` for bucketing.
472477
473478
:param exposure_kwargs: Additional arguments that will be passed
474479
to :code:`events_logger` (keys must be part of v2 event schema,
@@ -483,38 +488,29 @@ def get_variant_for_identifier(
483488
)
484489
return None
485490

486-
decider = self._get_decider()
487-
if decider is None:
488-
return None
489-
490-
ctx = self._get_ctx_with_set_identifier(
491-
identifier=identifier, identifier_type=identifier_type
492-
)
493-
ctx_err = ctx.err()
494-
if ctx_err is not None:
495-
logger.info(f"Encountered error in rust_decider.make_ctx(): {ctx_err}")
491+
if self._internal is None:
492+
logger.error("RustDecider is None--did not initialize.")
496493
return None
497494

498-
choice = decider.choose(
499-
feature_name=experiment_name, ctx=ctx, identifier_type=identifier_type
500-
)
501-
error = choice.err()
495+
ctx = self._decider_context.to_dict()
496+
ctx[identifier_type] = identifier
502497

503-
if error:
504-
logger.info(f"Encountered error in decider.choose(): {error}")
498+
try:
499+
decision = self._internal.choose(experiment_name, ctx)
500+
except FeatureNotFoundException as exc:
501+
warnings.warn(str(exc))
502+
return None
503+
except DeciderException as exc:
504+
logger.info(str(exc))
505505
return None
506-
507-
variant = choice.decision()
508506

509507
event_context_fields = self._decider_context.to_event_dict()
510508
event_context_fields.update(exposure_kwargs or {})
511509

512-
for event in choice.events():
513-
self._send_expose(
514-
event=event, exposure_fields=event_context_fields, overwrite_identifier=True
515-
)
510+
for event in decision.events:
511+
self._send_expose(event=event, exposure_fields=event_context_fields)
516512

517-
return variant
513+
return decision.variant
518514

519515
def get_variant_for_identifier_without_expose(
520516
self,
@@ -524,6 +520,14 @@ def get_variant_for_identifier_without_expose(
524520
) -> Optional[str]:
525521
"""Return a bucketing variant, if any, without emitting exposure event for a given :code:`identifier`.
526522
523+
Note: If the experiment's :code:`bucket_val` (e.g. "user_id", "device_id", "canonical_url")
524+
does not match the :code:`identifier_type` param,
525+
the :code:`identifier` will be ignored and not used to bucket (:code:`{identifier_type: identifier}` is
526+
added to internal :code:`DeciderContext` instance, but doesn't act like a bucketing override).
527+
528+
If the :code:`bucket_val` field exists on the :code:`DeciderContext` instance,
529+
that field will be used to bucket, since it corresponds to the experiment's config.
530+
527531
The :code:`expose()` function is available to be manually called afterward to emit
528532
exposure event.
529533
@@ -538,8 +542,11 @@ def get_variant_for_identifier_without_expose(
538542
:param identifier: an arbitary string used to bucket the experiment by
539543
being set on :code:`DeciderContext`'s :code:`identifier_type` field.
540544
541-
:param identifier_type: Sets :code:`{identifier_type: identifier}` on DeciderContext and
542-
should match an experiment's :code:`bucket_val` to get a variant.
545+
:param identifier_type: Sets :code:`{identifier_type: identifier}` on :code:`DeciderContext`.
546+
The experiment's :code:`bucket_val` will be looked up in :code:`DeciderContext` and be used to bucket.
547+
If the experiment's :code:`bucket_val` field does not match :code:`identifier_type` param,
548+
:code:`identifier` will be ignored and the field corresponding :code:`bucket_val` will be looked up
549+
from :code:`DeciderContext` for bucketing.
543550
544551
:return: Variant name if a variant is assigned, None otherwise.
545552
"""
@@ -549,37 +556,29 @@ def get_variant_for_identifier_without_expose(
549556
)
550557
return None
551558

552-
decider = self._get_decider()
553-
if decider is None:
559+
if self._internal is None:
560+
logger.error("RustDecider is None--did not initialize.")
554561
return None
555562

556-
ctx = self._get_ctx_with_set_identifier(
557-
identifier=identifier, identifier_type=identifier_type
558-
)
559-
ctx_err = ctx.err()
560-
if ctx_err is not None:
561-
logger.info(f"Encountered error in rust_decider.make_ctx(): {ctx_err}")
562-
return None
563+
ctx = self._decider_context.to_dict()
564+
ctx[identifier_type] = identifier
563565

564-
choice = decider.choose(
565-
feature_name=experiment_name, ctx=ctx, identifier_type=identifier_type
566-
)
567-
error = choice.err()
568-
if error:
569-
logger.info(f"Encountered error in decider.choose(): {error}")
566+
try:
567+
decision = self._internal.choose(experiment_name, ctx)
568+
except FeatureNotFoundException as exc:
569+
warnings.warn(str(exc))
570+
return None
571+
except DeciderException as exc:
572+
logger.info(str(exc))
570573
return None
571-
572-
variant = choice.decision()
573574

574575
event_context_fields = self._decider_context.to_event_dict()
575576

576577
# expose Holdout if the experiment is part of one
577-
for event in choice.events():
578-
self._send_expose_if_holdout(
579-
event=event, exposure_fields=event_context_fields, overwrite_identifier=True
580-
)
578+
for event in decision.events:
579+
self._send_expose_if_holdout(event=event, exposure_fields=event_context_fields)
581580

582-
return variant
581+
return decision.variant
583582

584583
def get_all_variants_without_expose(self) -> List[Dict[str, Union[str, int]]]:
585584
"""Return a list of experiment dicts in this format:
@@ -648,7 +647,8 @@ def _decision_to_dict(self, decision: Decision) -> Dict[str, Any]:
648647
def get_all_variants_for_identifier_without_expose(
649648
self, identifier: str, identifier_type: Literal["user_id", "device_id", "canonical_url"]
650649
) -> List[Dict[str, Union[str, int]]]:
651-
"""Return a list of experiment dicts for a given :code:`identifier` in this format:
650+
"""Return a list of experiment dicts for experiments having :code:`bucket_val` match
651+
:code:`identifier_type`, for a given :code:`identifier`, in this format:
652652
653653
.. code-block:: json
654654
@@ -675,7 +675,7 @@ def get_all_variants_for_identifier_without_expose(
675675
being set on :code:`DeciderContext`'s :code:`identifier_type` field.
676676
677677
:param identifier_type: Sets :code:`{identifier_type: identifier}` on DeciderContext and
678-
should match an experiment's :code:`bucket_val` to get a variant.
678+
buckets all experiment with matching :code:`bucket_val`.
679679
680680
:return: list of experiment dicts with non-:code:`None` variants.
681681
"""

tests/decider_tests.py

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -708,31 +708,6 @@ def test_get_variant_for_identifier_device_id(self):
708708
# `identifier` passed to correct event field of experiment's `bucket_val` config
709709
self.assertEqual(event_fields["device_id"], identifier)
710710

711-
def test_get_variant_for_identifier_wrong_bucket_val(self):
712-
identifier = USER_ID
713-
bucket_val = "device_id"
714-
self.exp_base_config["exp_1"]["experiment"].update({"bucket_val": bucket_val})
715-
716-
with create_temp_config_file(self.exp_base_config) as f:
717-
decider = setup_decider(f, self.dc, self.mock_span, self.event_logger)
718-
719-
self.assertEqual(self.event_logger.log.call_count, 0)
720-
with self.assertLogs() as captured:
721-
# `identifier_type="canonical_url"`, which doesn't match `bucket_val` of `device_id`
722-
variant = decider.get_variant_for_identifier(
723-
experiment_name="exp_1", identifier=identifier, identifier_type="canonical_url"
724-
)
725-
# `None` is returned since `identifier_type` doesn't match `bucket_val` in experiment-config json
726-
self.assertEqual(variant, None)
727-
# exposure isn't emitted either
728-
self.assertEqual(self.event_logger.log.call_count, 0)
729-
730-
assert any(
731-
'Encountered error in decider.choose(): Requested identifier_type "canonical_url" is incompatible with experiment\'s bucket_val = device_id'
732-
in x.getMessage()
733-
for x in captured.records
734-
)
735-
736711
def test_get_variant_for_identifier_bogus_identifier_type(self):
737712
identifier = "anything"
738713
identifier_type = "blah"
@@ -816,36 +791,6 @@ def test_get_variant_for_identifier_without_expose_user_id_for_holdout_exposure(
816791
experiment_name="hg", variant="holdout", event_fields=event_fields
817792
)
818793

819-
def test_get_variant_for_identifier_without_expose_for_holdout_exposure_wrong_bucket_val(self):
820-
identifier = DEVICE_ID
821-
bucket_val = "device_id"
822-
self.exp_base_config["exp_1"]["experiment"].update({"bucket_val": bucket_val})
823-
824-
self.exp_base_config["exp_1"].update({"parent_hg_name": "hg"})
825-
self.parent_hg_config["hg"]["experiment"].update({"bucket_val": bucket_val})
826-
self.exp_base_config.update(self.parent_hg_config)
827-
828-
with create_temp_config_file(self.exp_base_config) as f:
829-
decider = setup_decider(f, self.dc, self.mock_span, self.event_logger)
830-
831-
self.assertEqual(self.event_logger.log.call_count, 0)
832-
# `identifier_type="canonical_url"`, which doesn't match `bucket_val` of `device_id`
833-
self.assertEqual(self.event_logger.log.call_count, 0)
834-
with self.assertLogs() as captured:
835-
variant = decider.get_variant_for_identifier_without_expose(
836-
experiment_name="exp_1", identifier=identifier, identifier_type="canonical_url"
837-
)
838-
# `None` is returned since `identifier_type` doesn't match `bucket_val` in experiment-config json
839-
self.assertEqual(variant, None)
840-
841-
assert any(
842-
'Encountered error in decider.choose(): Requested identifier_type "canonical_url" is incompatible with experiment\'s bucket_val = device_id'
843-
in x.getMessage()
844-
for x in captured.records
845-
)
846-
847-
self.assertEqual(self.event_logger.log.call_count, 0)
848-
849794
def test_get_variant_for_identifier_without_expose_canonical_url(self):
850795
identifier = CANONICAL_URL
851796
bucket_val = "canonical_url"

0 commit comments

Comments
 (0)