Skip to content

Commit ea4e7fa

Browse files
authored
feat: add some platform info to events (#198)
1 parent 57a3e74 commit ea4e7fa

File tree

6 files changed

+163
-9
lines changed

6 files changed

+163
-9
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.16.0 - 2025-02-26
2+
3+
1. feat: add some platform info to events (#198)
4+
15
## 3.15.1 - 2025-02-23
26

37
1. Fix async client support for OpenAI.

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ Please see the [Python integration docs](https://posthog.com/docs/integrations/p
1010
### Testing Locally
1111

1212
1. Run `python3 -m venv env` (creates virtual environment called "env")
13+
* or `uv venv env`
1314
2. Run `source env/bin/activate` (activates the virtual environment)
1415
3. Run `python3 -m pip install -e ".[test]"` (installs the package in develop mode, along with test dependencies)
16+
* or `uv pip install -e ".[test]"`
1517
4. Run `make test`
1618
1. To run a specific test do `pytest -k test_no_api_key`
1719

posthog/client.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
import logging
33
import numbers
44
import os
5+
import platform
56
import sys
67
import warnings
78
from datetime import datetime, timedelta
9+
from typing import Any
810
from uuid import UUID, uuid4
911

12+
import distro # For Linux OS detection
1013
from dateutil.tz import tzutc
1114
from six import string_types
1215

@@ -29,6 +32,60 @@
2932
MAX_DICT_SIZE = 50_000
3033

3134

35+
def get_os_info():
36+
"""
37+
Returns standardized OS name and version information.
38+
Similar to how user agent parsing works in JS.
39+
"""
40+
os_name = ""
41+
os_version = ""
42+
43+
platform_name = sys.platform
44+
45+
if platform_name.startswith("win"):
46+
os_name = "Windows"
47+
if hasattr(platform, "win32_ver"):
48+
win_version = platform.win32_ver()[0]
49+
if win_version:
50+
os_version = win_version
51+
52+
elif platform_name == "darwin":
53+
os_name = "Mac OS X"
54+
if hasattr(platform, "mac_ver"):
55+
mac_version = platform.mac_ver()[0]
56+
if mac_version:
57+
os_version = mac_version
58+
59+
elif platform_name.startswith("linux"):
60+
os_name = "Linux"
61+
linux_info = distro.info()
62+
if linux_info["version"]:
63+
os_version = linux_info["version"]
64+
65+
elif platform_name.startswith("freebsd"):
66+
os_name = "FreeBSD"
67+
if hasattr(platform, "release"):
68+
os_version = platform.release()
69+
70+
else:
71+
os_name = platform_name
72+
if hasattr(platform, "release"):
73+
os_version = platform.release()
74+
75+
return os_name, os_version
76+
77+
78+
def system_context() -> dict[str, Any]:
79+
os_name, os_version = get_os_info()
80+
81+
return {
82+
"$python_runtime": platform.python_implementation(),
83+
"$python_version": "%s.%s.%s" % (sys.version_info[:3]),
84+
"$os": os_name,
85+
"$os_version": os_version,
86+
}
87+
88+
3289
class Client(object):
3390
"""Create a new PostHog client."""
3491

@@ -231,7 +288,8 @@ def capture(
231288
stacklevel=2,
232289
)
233290

234-
properties = properties or {}
291+
properties = {**(properties or {}), **system_context()}
292+
235293
require("distinct_id", distinct_id, ID_TYPES)
236294
require("properties", properties, dict)
237295
require("event", event, string_types)

posthog/test/test_client.py

+95-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import mock
77
import six
8+
from parameterized import parameterized
89

910
from posthog.client import Client
1011
from posthog.request import APIError
@@ -54,6 +55,11 @@ def test_basic_capture(self):
5455
self.assertEqual(msg["distinct_id"], "distinct_id")
5556
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
5657
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
58+
# these will change between platforms so just asssert on presence here
59+
assert msg["properties"]["$python_runtime"] == mock.ANY
60+
assert msg["properties"]["$python_version"] == mock.ANY
61+
assert msg["properties"]["$os"] == mock.ANY
62+
assert msg["properties"]["$os_version"] == mock.ANY
5763

5864
def test_basic_capture_with_uuid(self):
5965
client = self.client
@@ -101,7 +107,6 @@ def test_basic_super_properties(self):
101107
self.assertEqual(msg["properties"]["source"], "repo-name")
102108

103109
def test_basic_capture_exception(self):
104-
105110
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
106111
client = self.client
107112
exception = Exception("test exception")
@@ -129,7 +134,6 @@ def test_basic_capture_exception(self):
129134
)
130135

131136
def test_basic_capture_exception_with_distinct_id(self):
132-
133137
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
134138
client = self.client
135139
exception = Exception("test exception")
@@ -157,7 +161,6 @@ def test_basic_capture_exception_with_distinct_id(self):
157161
)
158162

159163
def test_basic_capture_exception_with_correct_host_generation(self):
160-
161164
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
162165
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://aloha.com")
163166
exception = Exception("test exception")
@@ -185,7 +188,6 @@ def test_basic_capture_exception_with_correct_host_generation(self):
185188
)
186189

187190
def test_basic_capture_exception_with_correct_host_generation_for_server_hosts(self):
188-
189191
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
190192
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://app.posthog.com")
191193
exception = Exception("test exception")
@@ -213,7 +215,6 @@ def test_basic_capture_exception_with_correct_host_generation_for_server_hosts(s
213215
)
214216

215217
def test_basic_capture_exception_with_no_exception_given(self):
216-
217218
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
218219
client = self.client
219220
try:
@@ -250,10 +251,8 @@ def test_basic_capture_exception_with_no_exception_given(self):
250251
self.assertEqual(capture_call[2]["$exception_list"][0]["stacktrace"]["frames"][0]["in_app"], True)
251252

252253
def test_basic_capture_exception_with_no_exception_happening(self):
253-
254254
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
255255
with self.assertLogs("posthog", level="WARNING") as logs:
256-
257256
client = self.client
258257
client.capture_exception()
259258

@@ -1124,3 +1123,92 @@ def test_default_properties_get_added_properly(self, patch_decide):
11241123
group_properties={},
11251124
disable_geoip=False,
11261125
)
1126+
1127+
@parameterized.expand(
1128+
[
1129+
# name, sys_platform, version_info, expected_runtime, expected_version, expected_os, expected_os_version, platform_method, platform_return, distro_info
1130+
(
1131+
"macOS",
1132+
"darwin",
1133+
(3, 8, 10),
1134+
"MockPython",
1135+
"3.8.10",
1136+
"Mac OS X",
1137+
"10.15.7",
1138+
"mac_ver",
1139+
("10.15.7", "", ""),
1140+
None,
1141+
),
1142+
(
1143+
"Windows",
1144+
"win32",
1145+
(3, 8, 10),
1146+
"MockPython",
1147+
"3.8.10",
1148+
"Windows",
1149+
"10",
1150+
"win32_ver",
1151+
("10", "", "", ""),
1152+
None,
1153+
),
1154+
(
1155+
"Linux",
1156+
"linux",
1157+
(3, 8, 10),
1158+
"MockPython",
1159+
"3.8.10",
1160+
"Linux",
1161+
"20.04",
1162+
None,
1163+
None,
1164+
{"version": "20.04"},
1165+
),
1166+
]
1167+
)
1168+
def test_mock_system_context(
1169+
self,
1170+
_name,
1171+
sys_platform,
1172+
version_info,
1173+
expected_runtime,
1174+
expected_version,
1175+
expected_os,
1176+
expected_os_version,
1177+
platform_method,
1178+
platform_return,
1179+
distro_info,
1180+
):
1181+
"""Test that we can mock platform and sys for testing system_context"""
1182+
with mock.patch("posthog.client.platform") as mock_platform:
1183+
with mock.patch("posthog.client.sys") as mock_sys:
1184+
# Set up common mocks
1185+
mock_platform.python_implementation.return_value = expected_runtime
1186+
mock_sys.version_info = version_info
1187+
mock_sys.platform = sys_platform
1188+
1189+
# Set up platform-specific mocks
1190+
if platform_method:
1191+
getattr(mock_platform, platform_method).return_value = platform_return
1192+
1193+
# Special handling for Linux which uses distro module
1194+
if sys_platform == "linux":
1195+
# Directly patch the get_os_info function to return our expected values
1196+
with mock.patch("posthog.client.get_os_info", return_value=(expected_os, expected_os_version)):
1197+
from posthog.client import system_context
1198+
1199+
context = system_context()
1200+
else:
1201+
# Get system context for non-Linux platforms
1202+
from posthog.client import system_context
1203+
1204+
context = system_context()
1205+
1206+
# Verify results
1207+
expected_context = {
1208+
"$python_runtime": expected_runtime,
1209+
"$python_version": expected_version,
1210+
"$os": expected_os,
1211+
"$os_version": expected_os_version,
1212+
}
1213+
1214+
assert context == expected_context

posthog/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "3.15.1"
1+
VERSION = "3.16.0"
22

33
if __name__ == "__main__":
44
print(VERSION, end="") # noqa: T201

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"monotonic>=1.5",
2121
"backoff>=1.10.0",
2222
"python-dateutil>2.1",
23+
"distro>=1.5.0", # Required for Linux OS detection in Python 3.9+
2324
]
2425

2526
extras_require = {
@@ -57,6 +58,7 @@
5758
"langchain-openai>=0.2.0",
5859
"langchain-anthropic>=0.2.0",
5960
"pydantic",
61+
"parameterized>=0.8.1",
6062
],
6163
"sentry": ["sentry-sdk", "django"],
6264
"langchain": ["langchain>=0.2.0"],

0 commit comments

Comments
 (0)