Skip to content

Commit 94f4227

Browse files
authored
fix(flags): keep dropped flag exposures out of the dedupe cache
Only mark a $feature_flag_called event as reported when capture() returns an event UUID. A should_capture rejection no longer suppresses the same exposure from a later allowed context on the shared client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: bdf93d58-c0c1-4a15-a1e6-78b6a3944b02
1 parent f339146 commit 94f4227

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

‎posthog/client.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4231,15 +4231,17 @@ def _capture_feature_flag_called_if_needed(
42314231
_MINIMAL_FLAG_CALLED_EVENT_PROPERTIES
42324232
)
42334233

4234-
self.capture(
4234+
event_uuid = self.capture(
42354235
"$feature_flag_called",
42364236
distinct_id=distinct_id,
42374237
properties=properties,
42384238
groups=groups or {},
42394239
disable_geoip=disable_geoip,
42404240
**extra_capture_kwargs,
42414241
)
4242-
reported_flags.add(feature_flag_reported_key)
4242+
# A dropped event was not reported, so a later allowed call must still send it.
4243+
if event_uuid is not None:
4244+
reported_flags.add(feature_flag_reported_key)
42434245

42444246
def get_remote_config_payload(self, key: str):
42454247
"""

‎posthog/test/test_should_capture.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,54 @@ def capture_in_context(private):
112112
assert client.disabled is False
113113

114114

115+
@pytest.mark.parametrize(
116+
"access_flag",
117+
[
118+
lambda client: client.get_feature_flag_result(
119+
"beta-feature", "user1", only_evaluate_locally=True
120+
),
121+
lambda client: client.evaluate_flags(
122+
"user1", only_evaluate_locally=True
123+
).is_enabled("beta-feature"),
124+
],
125+
ids=["get_feature_flag_result", "evaluate_flags"],
126+
)
127+
def test_rejected_flag_exposure_is_still_sent_from_an_allowed_context(access_flag):
128+
private_context = ContextVar("private_context", default=False)
129+
sent_events = []
130+
131+
def before_send(event):
132+
sent_events.append(event["event"])
133+
return event
134+
135+
client = Client(
136+
FAKE_TEST_API_KEY,
137+
secret_key="test",
138+
should_capture=lambda: not private_context.get(),
139+
before_send=before_send,
140+
send=False,
141+
)
142+
client.feature_flags = [
143+
{
144+
"id": 1,
145+
"key": "beta-feature",
146+
"active": True,
147+
"filters": {"groups": [{"properties": [], "rollout_percentage": 100}]},
148+
}
149+
]
150+
151+
token = private_context.set(True)
152+
try:
153+
access_flag(client)
154+
finally:
155+
private_context.reset(token)
156+
assert sent_events == []
157+
158+
access_flag(client)
159+
access_flag(client)
160+
assert sent_events == ["$feature_flag_called"]
161+
162+
115163
def test_filter_survives_client_reinitialization_after_fork():
116164
should_capture = mock.Mock(return_value=False)
117165
client = Client(FAKE_TEST_API_KEY, should_capture=should_capture, send=False)

0 commit comments

Comments
 (0)