Skip to content

Commit 551071c

Browse files
aapelivclaude
andcommitted
Rename global flag functions to get_global_*; clarify when to use them
Name the no-user functions get_global_boolean_value / get_global_string_value / ... so call sites read as explicitly global, and spell out in the comments that they're only for flags that genuinely can't be per-user (a toggle that behaves the same for everyone) - anything per-user must go through the context so rollouts and experiments can bucket by user. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a49a2a0 commit 551071c

3 files changed

Lines changed: 20 additions & 15 deletions

File tree

app/backend/src/couchers/experimentation.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
Two ways to evaluate a flag:
77
- Per-user/request: use the CouchersContext methods (context.get_boolean_value, get_string_value,
88
etc.), which evaluate for the context's user and own the per-request evaluator cache.
9-
- Global (no user/request): use the module-level get_boolean_value / get_string_value / ... below.
10-
These evaluate anonymously and are for background jobs and other code with no CouchersContext.
9+
- Global (no user/request): use the module-level get_global_boolean_value / get_global_string_value
10+
/ ... below. Only for flags whose value genuinely can't be per-user - a toggle that behaves the
11+
same for everyone. Anything that is or could be per-user must use the context so rollouts and
12+
experiments can bucket by user.
1113
1214
Both paths share the enabled / pass-all-gates gating helpers here. setup_experimentation() is called
1315
once at process startup.
@@ -197,24 +199,27 @@ def _boolean_value(flag_key: str, default: bool, get_evaluator: Callable[[], Gro
197199
return _feature_value(flag_key, default, get_evaluator)
198200

199201

200-
# Global (no user/request) flag evaluation. Mirrors the CouchersContext.get_*_value API but
201-
# evaluates anonymously: experiments and percentage rollouts are skipped (no user to bucket), so
202-
# flags fall through to their in-code defaults unless a rule forces a value globally.
203-
def get_boolean_value(flag_key: str, default: bool) -> bool:
202+
# Global (no-user) flag evaluation. Use these ONLY for a flag whose value genuinely cannot depend on
203+
# a user - one that toggles behavior the same way for everyone (e.g. a system-wide kill-switch or
204+
# behavior toggle). Anything that is or could be per-user must go through the CouchersContext methods
205+
# instead, so percentage rollouts and experiments can bucket by user. Here there is no user to
206+
# bucket, so rollouts and experiments are skipped and flags fall through to their in-code defaults
207+
# unless a rule forces a value globally.
208+
def get_global_boolean_value(flag_key: str, default: bool) -> bool:
204209
return _boolean_value(flag_key, default, _global_evaluator)
205210

206211

207-
def get_string_value(flag_key: str, default: str) -> str:
212+
def get_global_string_value(flag_key: str, default: str) -> str:
208213
return _feature_value(flag_key, default, _global_evaluator)
209214

210215

211-
def get_integer_value(flag_key: str, default: int) -> int:
216+
def get_global_integer_value(flag_key: str, default: int) -> int:
212217
return _feature_value(flag_key, default, _global_evaluator)
213218

214219

215-
def get_float_value(flag_key: str, default: float) -> float:
220+
def get_global_float_value(flag_key: str, default: float) -> float:
216221
return _feature_value(flag_key, default, _global_evaluator)
217222

218223

219-
def get_object_value[T](flag_key: str, default: T) -> T:
224+
def get_global_object_value[T](flag_key: str, default: T) -> T:
220225
return _feature_value(flag_key, default, _global_evaluator)

app/backend/src/couchers/jobs/handlers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -839,8 +839,8 @@ def update_badges(payload: empty_pb2.Empty) -> None:
839839

840840
def update_badge(badge_id: str, members: Sequence[int]) -> None:
841841
badge = get_badge_dict()[badge_id]
842-
# a flag that gates a badge is evaluated globally (no per-user request here)
843-
if badge.flag is not None and not experimentation.get_boolean_value(badge.flag, default=True):
842+
# a badge applies to everyone or no one, so its gate is a global (no-user) flag
843+
if badge.flag is not None and not experimentation.get_global_boolean_value(badge.flag, default=True):
844844
members = []
845845
user_ids = session.execute(select(UserBadge.user_id).where(UserBadge.badge_id == badge.id)).scalars().all()
846846
# in case the user ids don't exist in the db

app/backend/src/tests/test_experimentation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ def test_unknown_feature_returns_in_code_default(experimentation_snapshot):
4545

4646
def test_global_evaluation_excluded_from_rollout_gets_feature_default(experimentation_snapshot):
4747
# global (no-user) evaluation can't bucket into a rollout, so it gets the feature default
48-
assert experimentation.get_string_value("rollout_flag", "fallback") == "control"
48+
assert experimentation.get_global_string_value("rollout_flag", "fallback") == "control"
4949

5050

5151
def test_global_evaluation_gets_global_force_on_flag(experimentation_snapshot):
52-
assert experimentation.get_boolean_value("global_flag", default=False) is True
52+
assert experimentation.get_global_boolean_value("global_flag", default=False) is True
5353

5454

5555
def test_global_evaluation_unknown_feature_returns_in_code_default(experimentation_snapshot):
56-
assert experimentation.get_string_value("does_not_exist", "my_default") == "my_default"
56+
assert experimentation.get_global_string_value("does_not_exist", "my_default") == "my_default"

0 commit comments

Comments
 (0)