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
5 changes: 5 additions & 0 deletions .sampo/changesets/emit-is-server-property.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing a docstring entry above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, should be there for all others as well

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bool would be hard to change in the future without breaking changes if needed. Wondering if we should be defining as String (server, client for now) which will give us flexibility for the future. Based on the comment here for example this could also be "cli" (we'd need to rename the property of course to maybe something like $runtime_type)

I'll hold back review on other SDKs since they follow the same patters so these points can be discussed here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLI is also a client side, its all about if its running in the users machine or not at the end of the day, does not matter if its a desktop app, a mobile app, a CLI, etc
its either local or cloud, not sure if we need more than a boolean but good point

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea I think bool is ok, opening up to strings can make it complicated and we really just want to know if its server or not.

feature_flags_request_timeout_seconds = 3 # type: int
super_properties = None # type: Optional[Dict]
enable_exception_autocapture = False # type: bool
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment on lines +1338 to +1339
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, why do we omit vs set to False?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense to me, we dont send on the client side so its the same semantics


msg["distinct_id"] = stringify_id(msg.get("distinct_id", None))

msg = clean(msg)
Expand Down
34 changes: 34 additions & 0 deletions posthog/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
turnipdabeets marked this conversation as resolved.
# 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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
Loading