Skip to content

Commit 6276a9a

Browse files
committed
Implement user experiment holdouts
1 parent d396faf commit 6276a9a

File tree

2 files changed

+37
-5
lines changed

2 files changed

+37
-5
lines changed

discord/experiment.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ class GuildExperiment:
569569
An experiment that blocks the rollout of this experiment.
570570
aa_mode: :class:`bool`
571571
Whether the experiment is in A/A mode.
572-
trigger_debugging:
572+
trigger_debugging: :class:`bool`
573573
Whether experiment analytics trigger debugging is enabled.
574574
"""
575575

@@ -776,9 +776,12 @@ class UserExperiment:
776776
Whether the user has an explicit bucket override.
777777
population: :class:`int`
778778
The internal population group for the user, or None (-1) if manually overridden.
779+
holdout: Optional[:class:`UserExperiment`]
780+
An experiment that blocks the rollout of this experiment.
781+
Only present if the user is in the holdout.
779782
aa_mode: :class:`bool`
780783
Whether the experiment is in A/A mode.
781-
trigger_debugging:
784+
trigger_debugging: :class:`bool`
782785
Whether experiment analytics trigger debugging is enabled.
783786
"""
784787

@@ -793,10 +796,11 @@ class UserExperiment:
793796
'_result',
794797
'aa_mode',
795798
'trigger_debugging',
799+
'holdout',
796800
)
797801

798802
def __init__(self, *, state: ConnectionState, data: AssignmentPayload):
799-
(hash, revision, bucket, override, population, hash_result, aa_mode, trigger_debugging, *_) = data
803+
(hash, revision, bucket, override, population, hash_result, aa_mode, trigger_debugging, holdout_name, holdout_revision, holdout_bucket, *_) = data
800804

801805
self._state = state
802806
self._name: Optional[str] = None
@@ -809,6 +813,29 @@ def __init__(self, *, state: ConnectionState, data: AssignmentPayload):
809813
self.aa_mode: bool = aa_mode == 1
810814
self.trigger_debugging: bool = trigger_debugging == 1
811815

816+
self.holdout: Optional[UserExperiment] = None
817+
if holdout_name is not None:
818+
holdout_hash = murmurhash32(holdout_name, signed=False)
819+
self.holdout = state.experiments.get(holdout_hash) or UserExperiment.from_holdout(state, holdout_hash, holdout_name, holdout_revision, holdout_bucket)
820+
821+
@classmethod
822+
def from_holdout(cls, state: ConnectionState, hash: int, name: str, revision: Optional[int], bucket: Optional[int]) -> UserExperiment:
823+
self = cls.__new__(cls)
824+
self._state = state
825+
self._name = name
826+
self.hash = hash
827+
self.revision = revision or 0
828+
self.assignment = bucket or -1
829+
830+
# Most of these fields are defaults
831+
self.override = False
832+
self.population = 0
833+
self._result = -1
834+
self.aa_mode = False
835+
self.trigger_debugging = False
836+
self.holdout = None
837+
return self
838+
812839
def __repr__(self) -> str:
813840
return f'<UserExperiment hash={self.hash}{f" name={self._name!r}" if self._name else ""} bucket={self.bucket}>'
814841

@@ -855,9 +882,11 @@ def result(self) -> int:
855882
:exc:`ValueError`
856883
The experiment name is unset without a precomputed result.
857884
"""
858-
if self._result:
885+
if self._result >= 0:
859886
return self._result
860887
elif not self.name:
861888
raise ValueError('The experiment name must be set to compute the result')
862889
else:
863-
return murmurhash32(f'{self.name}:{self._state.self_id}', signed=False) % 10000
890+
result = murmurhash32(f'{self.name}:{self._state.self_id}', signed=False) % 10000
891+
self._result = result
892+
return result

discord/types/experiment.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ class Override(TypedDict):
8989
int, # hash_result
9090
Literal[0, 1], # aa_mode
9191
Literal[0, 1], # trigger_debugging
92+
Optional[str], # holdout_name
93+
Optional[int], # holdout_revision
94+
Optional[int], # holdout_bucket
9295
]
9396

9497

0 commit comments

Comments
 (0)