diff --git a/.sampo/changesets/emit-is-server-property.md b/.sampo/changesets/emit-is-server-property.md new file mode 100644 index 00000000..e3c05a24 --- /dev/null +++ b/.sampo/changesets/emit-is-server-property.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: minor +--- + +Add a configurable `$is_server` event property (default `true`) so PostHog can identify server-side events. Set `is_server=False` when using posthog-python as a client/CLI so the device OS is attributed normally. diff --git a/posthog/__init__.py b/posthog/__init__.py index 559aba88..892a68c6 100644 --- a/posthog/__init__.py +++ b/posthog/__init__.py @@ -282,6 +282,9 @@ def get_tags() -> Dict[str, Any]: poll_interval: Seconds between local feature flag definition refreshes. disable_geoip: Whether to disable server-side GeoIP enrichment. Defaults to True. + is_server: Whether events are emitted from a server-side runtime. Defaults to + True; set to False when using the SDK as a client/CLI so the device OS is + attributed to the person normally. feature_flags_request_timeout_seconds: Timeout in seconds for feature flag and remote config requests. super_properties: Properties merged into every captured event. @@ -313,6 +316,7 @@ def get_tags() -> Dict[str, Any]: project_api_key = None # type: Optional[str] poll_interval = 30 # type: int disable_geoip = True # type: bool +is_server = True # type: bool feature_flags_request_timeout_seconds = 3 # type: int super_properties = None # type: Optional[Dict] enable_exception_autocapture = False # type: bool @@ -1084,6 +1088,7 @@ def setup() -> Client: poll_interval=poll_interval, disabled=disabled, disable_geoip=disable_geoip, + is_server=is_server, feature_flags_request_timeout_seconds=feature_flags_request_timeout_seconds, super_properties=super_properties, # TODO: Currently this monitoring begins only when the Client is initialised (which happens when you do something with the SDK) diff --git a/posthog/client.py b/posthog/client.py index d2cccc26..bf967449 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -194,6 +194,7 @@ def __init__( personal_api_key=None, disabled=False, disable_geoip=True, + is_server=True, historical_migration=False, feature_flags_request_timeout_seconds=3, super_properties=None, @@ -239,6 +240,9 @@ def __init__( disabled: If True, disable captures and API requests. Useful in tests. disable_geoip: Whether to disable server-side GeoIP enrichment. Defaults to True. + is_server: Whether events are emitted from a server-side runtime. + Defaults to True; set to False when using the SDK as a client/CLI + so the device OS is attributed to the person normally. historical_migration: Mark events as historical migration imports. feature_flags_request_timeout_seconds: Timeout in seconds for feature flag and remote config requests. @@ -314,6 +318,7 @@ def __init__( self._flag_definition_cache_provider_async_runner_lock = threading.Lock() self.disabled = disabled or not self.api_key self.disable_geoip = disable_geoip + self.is_server = is_server self.historical_migration = historical_migration self.super_properties = super_properties self.enable_exception_autocapture = enable_exception_autocapture @@ -1328,6 +1333,11 @@ def _enqueue(self, msg, disable_geoip): if self.super_properties: msg["properties"] = {**msg["properties"], **self.super_properties} + # Set after the super_properties merge so this SDK's server classification + # can't be silently overridden by a user-provided super property. + if self.is_server: + msg["properties"]["$is_server"] = True + msg["distinct_id"] = stringify_id(msg.get("distinct_id", None)) msg = clean(msg) diff --git a/posthog/test/test_client.py b/posthog/test/test_client.py index 4fac43c8..e9d7054c 100644 --- a/posthog/test/test_client.py +++ b/posthog/test/test_client.py @@ -159,12 +159,42 @@ def test_basic_capture(self): self.assertEqual(msg["distinct_id"], "distinct_id") self.assertEqual(msg["properties"]["$lib"], "posthog-python") self.assertEqual(msg["properties"]["$lib_version"], VERSION) + self.assertEqual(msg["properties"]["$is_server"], True) # these will change between platforms so just asssert on presence here assert msg["properties"]["$python_runtime"] == mock.ANY assert msg["properties"]["$python_version"] == mock.ANY assert msg["properties"]["$os"] == mock.ANY assert msg["properties"]["$os_version"] == mock.ANY + def test_capture_omits_is_server_when_disabled(self): + with mock.patch("posthog.client.batch_post") as mock_post: + client = Client( + FAKE_TEST_API_KEY, + on_error=self.set_fail, + sync_mode=True, + is_server=False, + ) + client.capture("python test event", distinct_id="distinct_id") + self.assertFalse(self.failed) + + msg = mock_post.call_args[1]["batch"][0] + self.assertEqual(msg["properties"]["$lib"], "posthog-python") + self.assertNotIn("$is_server", msg["properties"]) + + def test_is_server_not_overridden_by_super_properties(self): + with mock.patch("posthog.client.batch_post") as mock_post: + client = Client( + FAKE_TEST_API_KEY, + on_error=self.set_fail, + sync_mode=True, + super_properties={"$is_server": False}, + ) + client.capture("python test event", distinct_id="distinct_id") + self.assertFalse(self.failed) + + msg = mock_post.call_args[1]["batch"][0] + self.assertEqual(msg["properties"]["$is_server"], True) + def test_basic_capture_with_uuid(self): with mock.patch("posthog.client.batch_post") as mock_post: client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True) @@ -1391,6 +1421,7 @@ def test_basic_group_identify(self): "$lib": "posthog-python", "$lib_version": VERSION, "$geoip_disable": True, + "$is_server": True, }, ) self.assertTrue(isinstance(msg["timestamp"], str)) @@ -1420,6 +1451,7 @@ def test_basic_group_identify_with_distinct_id(self): "$lib": "posthog-python", "$lib_version": VERSION, "$geoip_disable": True, + "$is_server": True, }, ) self.assertTrue(isinstance(msg["timestamp"], str)) @@ -1453,6 +1485,7 @@ def test_advanced_group_identify(self): "$lib": "posthog-python", "$lib_version": VERSION, "$geoip_disable": True, + "$is_server": True, }, ) self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00") @@ -1488,6 +1521,7 @@ def test_advanced_group_identify_with_distinct_id(self): "$lib": "posthog-python", "$lib_version": VERSION, "$geoip_disable": True, + "$is_server": True, }, ) self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")