Skip to content

Commit 914f4a5

Browse files
authored
Merge pull request #118 from reddit/add_measured_rollout_support
Support Measured Rollouts and retire "owner" field
2 parents 0f3a0f7 + 1f4222e commit 914f4a5

File tree

5 files changed

+84
-14
lines changed

5 files changed

+84
-14
lines changed

docs/requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ docutils==0.16
44
Jinja2==2.11.2
55
MarkupSafe==1.1.1
66
pydocstyle==5.1.1
7-
reddit-decider==1.7.0
7+
reddit-decider==1.11.0
88
reddit-edgecontext==1.0.0a3
99
Sphinx==3.4.0
1010
sphinx-autodoc-typehints==1.11.1

reddit_decider/__init__.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ class ExperimentConfig:
7373
bucket_val: str
7474
start_ts: int
7575
stop_ts: int
76-
owner: str
76+
owner: None = None
7777
emit_event: Optional[bool] = None
78+
measured: Optional[bool] = None
7879

7980

8081
class DeciderContext:
@@ -236,7 +237,6 @@ def _send_expose(self, event: str, exposure_fields: dict) -> None:
236237
bucket_val,
237238
start_ts,
238239
stop_ts,
239-
owner,
240240
) = event.split("::::")
241241
except ValueError:
242242
logger.warning(
@@ -251,7 +251,6 @@ def _send_expose(self, event: str, exposure_fields: dict) -> None:
251251
bucket_val=bucket_val,
252252
start_ts=self._cast_to_int(start_ts),
253253
stop_ts=self._cast_to_int(stop_ts),
254-
owner=owner,
255254
)
256255

257256
event_fields = {**event_fields, **{bucket_val: bucketing_value}}
@@ -279,7 +278,6 @@ def _send_expose_if_holdout(self, event: str, exposure_fields: dict) -> None:
279278
bucket_val,
280279
start_ts,
281280
stop_ts,
282-
owner,
283281
) = event.split("::::")
284282
except ValueError:
285283
logger.warning(
@@ -299,7 +297,6 @@ def _send_expose_if_holdout(self, event: str, exposure_fields: dict) -> None:
299297
bucket_val=bucket_val,
300298
start_ts=self._cast_to_int(start_ts),
301299
stop_ts=self._cast_to_int(stop_ts),
302-
owner=owner,
303300
)
304301

305302
event_fields = {**event_fields, **{bucket_val: bucketing_value}}
@@ -432,7 +429,6 @@ def expose(
432429
bucket_val=feature.bucket_val,
433430
start_ts=feature.start_ts,
434431
stop_ts=feature.stop_ts,
435-
owner=feature.owner,
436432
)
437433

438434
self._event_logger.log(
@@ -920,8 +916,8 @@ def get_experiment(self, experiment_name: str) -> Optional[ExperimentConfig]:
920916
bucket_val=feature.bucket_val,
921917
start_ts=feature.start_ts,
922918
stop_ts=feature.stop_ts,
923-
owner=feature.owner,
924919
emit_event=feature.emit_event,
920+
measured=feature.measured,
925921
)
926922

927923

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-r requirements-transitive.txt
22
baseplate>=2.0.0a1,<3.0
33
black==21.4b2
4-
reddit-decider==1.7.0
4+
reddit-decider==1.11.0
55
flake8==3.9.1
66
mypy==0.790
77
prometheus-client>=0.12.0,<1.0

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
install_requires=[
2020
"baseplate>=2.0.0a1,<3.0",
2121
"reddit-edgecontext>=1.0.0a3,<2.0",
22-
"reddit-decider~=1.7.0",
22+
"reddit-decider~=1.11.0",
2323
"typing_extensions>=3.10.0.0,<5.0",
2424
],
2525
package_data={"reddit_experiments": ["py.typed"], "reddit_decider": ["py.typed"]},

tests/decider_tests.py

+78-4
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ def assert_exposure_event_fields(
500500
cfg = self.exp_base_config[experiment_name]
501501
self.assertEqual(getattr(event_fields["experiment"], "id"), cfg["id"])
502502
self.assertEqual(getattr(event_fields["experiment"], "name"), cfg["name"])
503-
self.assertEqual(getattr(event_fields["experiment"], "owner"), cfg["owner"])
503+
self.assertEqual(getattr(event_fields["experiment"], "owner"), None)
504504
self.assertEqual(getattr(event_fields["experiment"], "version"), cfg["version"])
505505
self.assertEqual(getattr(event_fields["experiment"], "bucket_val"), bucket_val)
506506

@@ -520,7 +520,7 @@ def assert_minimal_exposure_event_fields(
520520
cfg = self.exp_base_config[experiment_name]
521521
self.assertEqual(getattr(event_fields["experiment"], "id"), cfg["id"])
522522
self.assertEqual(getattr(event_fields["experiment"], "name"), cfg["name"])
523-
self.assertEqual(getattr(event_fields["experiment"], "owner"), cfg["owner"])
523+
self.assertEqual(getattr(event_fields["experiment"], "owner"), None)
524524
self.assertEqual(getattr(event_fields["experiment"], "version"), cfg["version"])
525525
self.assertEqual(getattr(event_fields["experiment"], "bucket_val"), bucket_val)
526526

@@ -573,7 +573,7 @@ def test_none_returned_on_get_variant_call_with_bad_id(self):
573573
self.assertEqual(self.event_logger.log.call_count, 0)
574574

575575
assert any(
576-
"Partially loaded Decider: 1 features failed to load: {'test': 'Manifest parsing error: invalid type: string \"1\", expected u32'}"
576+
'Partially loaded Decider: 1 feature(s) failed to load: {"test": ParsingError(Error("invalid type: string \\"1\\", expected u32", line: 0, column: 0))'
577577
in x.getMessage()
578578
for x in captured.records
579579
)
@@ -1508,6 +1508,81 @@ def test_get_variant_with_disabled_exp(self):
15081508
# exposure assertions
15091509
self.assertEqual(self.event_logger.log.call_count, 0)
15101510

1511+
def test_range_variant_emit_event_override(self):
1512+
cfg = {
1513+
"feature_rollout_100": {
1514+
"id": 9110,
1515+
"name": "feature_rollout_100",
1516+
"enabled": True,
1517+
"version": "1",
1518+
"start_ts": 1522306800,
1519+
"stop_ts": 32533405261,
1520+
"owner": "[email protected]",
1521+
"type": "feature_rollout",
1522+
"emit_event": False,
1523+
"experiment": {
1524+
"variants": [
1525+
{"name": "enabled", "size": 1.0, "range_end": 1.0, "range_start": 0.0}
1526+
],
1527+
"experiment_version": 1,
1528+
"shuffle_version": 0,
1529+
"bucket_val": "user_id",
1530+
"log_bucketing": False,
1531+
},
1532+
},
1533+
"measured_rollout_100": {
1534+
"id": 9119,
1535+
"name": "measured_rollout_100",
1536+
"enabled": True,
1537+
"version": "1",
1538+
"start_ts": 1522306800,
1539+
"stop_ts": 32533405261,
1540+
"owner": "[email protected]",
1541+
"type": "range_variant",
1542+
"emit_event": False,
1543+
"measured": True,
1544+
"experiment": {
1545+
"variants": [
1546+
{
1547+
"name": "enabled",
1548+
"size": 1.0,
1549+
"range_end": 1.0,
1550+
"range_start": 0.0,
1551+
"emit_event_override": True,
1552+
}
1553+
],
1554+
"experiment_version": 1,
1555+
"shuffle_version": 0,
1556+
"bucket_val": "user_id",
1557+
"log_bucketing": False,
1558+
},
1559+
},
1560+
}
1561+
1562+
with create_temp_config_file(cfg) as f:
1563+
decider = setup_decider(f, self.dc, self.mock_span, self.event_logger)
1564+
1565+
self.assertEqual(self.event_logger.log.call_count, 0)
1566+
variant = decider.get_variant(experiment_name="feature_rollout_100")
1567+
self.assertEqual(variant, "enabled")
1568+
1569+
# FR does NOT emit exposure
1570+
self.assertEqual(self.event_logger.log.call_count, 0)
1571+
1572+
# FR is NOT "measured"
1573+
fr_cfg = decider.get_experiment(experiment_name="feature_rollout_100")
1574+
self.assertEqual(fr_cfg.measured, False)
1575+
1576+
variant = decider.get_variant(experiment_name="measured_rollout_100")
1577+
self.assertEqual(variant, "enabled")
1578+
1579+
# Measured Rollout DOES emit exposure (due to RV "emit_event_override" field)
1580+
self.assertEqual(self.event_logger.log.call_count, 1)
1581+
1582+
# MR IS "measured"
1583+
mr_cfg = decider.get_experiment(experiment_name="measured_rollout_100")
1584+
self.assertEqual(mr_cfg.measured, True)
1585+
15111586
def test_get_experiment(self):
15121587
with create_temp_config_file(self.exp_base_config) as f:
15131588
decider = setup_decider(f, self.dc, self.mock_span, self.event_logger)
@@ -1521,7 +1596,6 @@ def test_get_experiment(self):
15211596
self.assertEqual(experiment.bucket_val, cfg["experiment"]["bucket_val"])
15221597
self.assertEqual(experiment.start_ts, cfg["start_ts"])
15231598
self.assertEqual(experiment.stop_ts, cfg["stop_ts"])
1524-
self.assertEqual(experiment.owner, cfg["owner"])
15251599
self.assertEqual(experiment.emit_event, True)
15261600

15271601
def test_get_variant_without_expose_with_HG_as_control_1_and_child_returns_none_does_expose(

0 commit comments

Comments
 (0)