Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/migration-7.x-to-8.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ Values apply in this order. Steps 2 to 4 fill only the options and properties th
1. the `options` and `properties` of the call, then the call's `disable_geoip` argument, which fills `$geoip_disable`
2. context options and tags
3. `super_options` and `super_properties`
4. values the SDK sets: `$is_server` from `is_server`, `$geoip_disable` from the client's `disable_geoip` setting, system properties such as `$os` and `$python_version`, `options.process_person_profile = false` for events without a distinct ID, and `$release_id` from `POSTHOG_RELEASE_ID`
4. values the SDK sets: `$is_server` from `is_server`, `$geoip_disable` from the client's `disable_geoip` setting, system properties such as `$os` and `$python_version`, the `$feature/<key>` and `$active_feature_flags` properties from `flags` or `send_feature_flags`, `options.process_person_profile = false` for events without a distinct ID, and `$release_id` from `POSTHOG_RELEASE_ID`
5. `before_send`, which sees the result of steps 1 to 4 and can change or remove any of it
6. legacy properties, which fill unset options and are then removed

Expand All @@ -203,6 +203,7 @@ These changes follow from this order:

- Properties passed to a call now override `super_properties`. `super_properties` can no longer change `$lib` or `$lib_version`.
- A `$is_server`, `$geoip_disable` or system property such as `$os` that you set in a call or a context tag now wins over the SDK's value. In 7.x the SDK overwrote it. `super_properties` win over the SDK's value too, so `super_properties={"$geoip_disable": False}` turns GeoIP lookup on for events, even with `disable_geoip=True`.
- Feature flag properties from `flags` or `send_feature_flags` still lose to the same key in the call's `properties`, a context tag or `super_properties`, as in 7.x.
- A `disable_geoip` argument to a call belongs to that event, so it wins over context tags and `super_properties`. A `$geoip_disable` in the call's own `properties` still wins over it. `disable_geoip=False` now sends `$geoip_disable: false`. In 7.x it sent no `$geoip_disable` property.
- The `groups` argument merges into a `$groups` property of the call, and wins key by key. In 7.x it replaced the property. MCP events merge their identity's groups into a custom `$groups` property the same way.
- A `$set`, `$set_once`, `$groups` or `$group_set` in `super_properties` no longer replaces the whole value of the call. The two merge, and the call wins key by key.
Expand Down
6 changes: 5 additions & 1 deletion posthog/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ def _event_defaults(
property_allowlist=None,
disable_geoip: Optional[bool] = None,
system_properties: Optional[dict[str, Any]] = None,
flag_properties: Optional[dict[str, Any]] = None,
) -> _EventDefaults:
return _build_event_defaults(
super_properties=self.super_properties,
Expand All @@ -559,6 +560,7 @@ def _event_defaults(
disable_geoip=self.disable_geoip,
call_disable_geoip=disable_geoip,
system_properties=system_properties,
flag_properties=flag_properties,
)

def _build_capture_event(
Expand All @@ -576,8 +578,9 @@ def _build_capture_event(

flags_snapshot = kwargs.get("flags")
send_feature_flags = kwargs.get("send_feature_flags")
flag_properties = None
if flags_snapshot is not None:
properties = {**flags_snapshot._get_event_properties(), **properties}
flag_properties = flags_snapshot._get_event_properties()
elif send_feature_flags:
warnings.warn(
"AsyncClient does not support deprecated send_feature_flags. Pass a "
Expand All @@ -603,6 +606,7 @@ def _build_capture_event(
property_allowlist=kwargs.get("_property_allowlist"),
disable_geoip=kwargs.get("disable_geoip"),
system_properties=system_context(),
flag_properties=flag_properties,
),
)

Expand Down
8 changes: 6 additions & 2 deletions posthog/capture_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,23 @@ def _build_event_defaults(
disable_geoip: bool = False,
call_disable_geoip: Optional[bool] = None,
system_properties: Optional[Mapping[str, Any]] = None,
flag_properties: Optional[Mapping[str, Any]] = None,
) -> _EventDefaults:
"""Order the layers: per-call arguments, context, global, then SDK values.

``disable_geoip`` is the client setting, an SDK value. ``call_disable_geoip``
is the argument of one call: it is a value of that event, so only the
event's own ``$geoip_disable`` property beats it.
event's own ``$geoip_disable`` property beats it. ``flag_properties`` (the
``$feature/*`` and ``$active_feature_flags`` values from ``flags`` or
``send_feature_flags``) are SDK values, so context and super properties
beat them.
"""
call_properties = (
{} if call_disable_geoip is None else {"$geoip_disable": call_disable_geoip}
)
# A value in the event, the context or super properties wins over every
# value the SDK adds, including `$is_server` and `$geoip_disable`.
sdk_properties = dict(system_properties or {})
sdk_properties = {**(flag_properties or {}), **(system_properties or {})}
if release_id is not None:
sdk_properties["$release_id"] = release_id
if is_server:
Expand Down
7 changes: 3 additions & 4 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1911,10 +1911,6 @@ def _capture(
if active_feature_flags:
extra_properties["$active_feature_flags"] = active_feature_flags

if extra_properties:
properties = {**extra_properties, **properties}
msg["properties"] = properties

return self._enqueue(
msg,
disable_geoip,
Expand All @@ -1924,6 +1920,7 @@ def _capture(
context_options=_context_get_context_options(),
derived_options=_personless_options(personless),
system_properties=system_context(),
flag_properties=extra_properties,
)

def _parse_send_feature_flags(self, send_feature_flags) -> SendFeatureFlagsOptions:
Expand Down Expand Up @@ -2538,6 +2535,7 @@ def _enqueue(
context_options=None,
derived_options=None,
system_properties=None,
flag_properties=None,
):
# type: (...) -> Optional[str]
"""Push a new `msg` onto a lane's queue (analytics when unspecified), return the event uuid or None."""
Expand Down Expand Up @@ -2585,6 +2583,7 @@ def _enqueue(
disable_geoip=self.disable_geoip,
call_disable_geoip=disable_geoip,
system_properties=system_properties,
flag_properties=flag_properties,
),
)

Expand Down
4 changes: 2 additions & 2 deletions posthog/test/test_evaluate_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ def test_capture_with_flags_attaches_feature_properties(
None,
)
self.assertIsNotNone(page_viewed)
properties = page_viewed[0][0]["properties"]
properties = page_viewed.kwargs["flag_properties"]
self.assertEqual(properties["$feature/variant-flag"], "variant-value")
self.assertEqual(properties["$feature/boolean-flag"], True)
self.assertEqual(properties["$feature/disabled-flag"], False)
Expand Down Expand Up @@ -659,7 +659,7 @@ def test_capture_with_only_accessed_attaches_only_those_flags(
),
None,
)
properties = page_viewed[0][0]["properties"]
properties = page_viewed.kwargs["flag_properties"]
self.assertEqual(properties["$feature/boolean-flag"], True)
self.assertNotIn("$feature/variant-flag", properties)
self.assertNotIn("$feature/disabled-flag", properties)
Expand Down
48 changes: 48 additions & 0 deletions posthog/test/test_event_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,54 @@ async def test_async_caller_values_beat_sdk_values(source):
_assert_caller_values(events, CALLER_SDK_VALUES)


class _FlagSnapshot:
def _get_event_properties(self):
return {"$feature/beta": "snapshot", "$active_feature_flags": ["beta"]}


def _capture_with_flags_and_caller_value(source):
def call(client):
with new_context(fresh=True):
if source == "context":
tag("$feature/beta", "caller")
properties = {"$feature/beta": "caller"} if source == "event" else None
return client.capture(
"e", distinct_id="u", properties=properties, flags=_FlagSnapshot()
)

return call


def _flag_layer_config(source):
return (
{"super_properties": {"$feature/beta": "caller"}} if source == "super" else {}
)


def _assert_flag_layers(events, source):
properties = events[0]["properties"]
expected = "snapshot" if source is None else "caller"
assert properties["$feature/beta"] == expected
assert properties["$active_feature_flags"] == ["beta"]


@pytest.mark.parametrize("source", ["event", "context", "super", None])
def test_sync_caller_layers_beat_flag_properties(source):
events = _sync_wire_events(
_capture_with_flags_and_caller_value(source), **_flag_layer_config(source)
)
_assert_flag_layers(events, source)


@pytest.mark.asyncio
@pytest.mark.parametrize("source", ["event", "context", "super", None])
async def test_async_caller_layers_beat_flag_properties(source):
events = await _async_wire_events(
_capture_with_flags_and_caller_value(source), **_flag_layer_config(source)
)
_assert_flag_layers(events, source)


GEOIP_CALL_CASES = [
pytest.param(True, "context", False, True, id="call_beats_context"),
pytest.param(True, "super", False, True, id="call_beats_super"),
Expand Down
Loading