2357 lines
89 KiB
Python
2357 lines
89 KiB
Python
import time
|
|
import unittest
|
|
from datetime import datetime
|
|
from uuid import uuid4
|
|
|
|
import mock
|
|
import six
|
|
from parameterized import parameterized
|
|
|
|
from posthog.client import Client
|
|
from posthog.contexts import get_context_session_id, new_context, set_context_session
|
|
from posthog.request import APIError
|
|
from posthog.test.test_utils import FAKE_TEST_API_KEY
|
|
from posthog.types import FeatureFlag, LegacyFlagMetadata
|
|
from posthog.version import VERSION
|
|
|
|
|
|
class TestClient(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
# This ensures no real HTTP POST requests are made
|
|
cls.client_post_patcher = mock.patch("posthog.client.batch_post")
|
|
cls.consumer_post_patcher = mock.patch("posthog.consumer.batch_post")
|
|
cls.client_post_patcher.start()
|
|
cls.consumer_post_patcher.start()
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.client_post_patcher.stop()
|
|
cls.consumer_post_patcher.stop()
|
|
|
|
def set_fail(self, e, batch):
|
|
"""Mark the failure handler"""
|
|
print("FAIL", e, batch) # noqa: T201
|
|
self.failed = True
|
|
|
|
def setUp(self):
|
|
self.failed = False
|
|
self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
|
|
|
|
def test_requires_api_key(self):
|
|
self.assertRaises(TypeError, Client)
|
|
|
|
def test_empty_flush(self):
|
|
self.client.flush()
|
|
|
|
def test_basic_capture(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)
|
|
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
# 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_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)
|
|
uuid = str(uuid4())
|
|
msg_uuid = client.capture(
|
|
"python test event", distinct_id="distinct_id", uuid=uuid
|
|
)
|
|
self.assertEqual(msg_uuid, uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertEqual(msg["uuid"], uuid)
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
|
|
def test_basic_capture_with_project_api_key(self):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
project_api_key=FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
sync_mode=True,
|
|
)
|
|
|
|
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
|
|
def test_basic_super_properties(self):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
super_properties={"source": "repo-name"},
|
|
sync_mode=True,
|
|
)
|
|
|
|
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Check the enqueued message
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertEqual(msg["properties"]["source"], "repo-name")
|
|
|
|
def test_basic_capture_exception(self):
|
|
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
|
|
client = self.client
|
|
exception = Exception("test exception")
|
|
client.capture_exception(exception, distinct_id="distinct_id")
|
|
|
|
self.assertTrue(patch_capture.called)
|
|
capture_call = patch_capture.call_args
|
|
self.assertEqual(capture_call[0][0], "$exception")
|
|
self.assertEqual(capture_call[1]["distinct_id"], "distinct_id")
|
|
|
|
def test_basic_capture_exception_with_distinct_id(self):
|
|
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
|
|
client = self.client
|
|
exception = Exception("test exception")
|
|
client.capture_exception(exception, distinct_id="distinct_id")
|
|
|
|
self.assertTrue(patch_capture.called)
|
|
capture_call = patch_capture.call_args
|
|
self.assertEqual(capture_call[0][0], "$exception")
|
|
self.assertEqual(capture_call[1]["distinct_id"], "distinct_id")
|
|
|
|
def test_basic_capture_exception_with_correct_host_generation(self):
|
|
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://aloha.com"
|
|
)
|
|
exception = Exception("test exception")
|
|
client.capture_exception(exception, distinct_id="distinct_id")
|
|
|
|
self.assertTrue(patch_capture.called)
|
|
call = patch_capture.call_args
|
|
self.assertEqual(call[0][0], "$exception")
|
|
self.assertEqual(call[1]["distinct_id"], "distinct_id")
|
|
|
|
def test_basic_capture_exception_with_correct_host_generation_for_server_hosts(
|
|
self,
|
|
):
|
|
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
host="https://app.posthog.com",
|
|
)
|
|
exception = Exception("test exception")
|
|
client.capture_exception(exception, distinct_id="distinct_id")
|
|
|
|
self.assertTrue(patch_capture.called)
|
|
capture_call = patch_capture.call_args
|
|
self.assertEqual(capture_call[0][0], "$exception")
|
|
self.assertEqual(capture_call[1]["distinct_id"], "distinct_id")
|
|
|
|
def test_basic_capture_exception_with_no_exception_given(self):
|
|
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
|
|
client = self.client
|
|
try:
|
|
raise Exception("test exception")
|
|
except Exception:
|
|
client.capture_exception(None, distinct_id="distinct_id")
|
|
|
|
self.assertTrue(patch_capture.called)
|
|
capture_call = patch_capture.call_args
|
|
print(capture_call)
|
|
self.assertEqual(capture_call[1]["distinct_id"], "distinct_id")
|
|
self.assertEqual(capture_call[0][0], "$exception")
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_type"], "Exception"
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_message"], "test exception"
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["mechanism"][
|
|
"type"
|
|
],
|
|
"generic",
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["mechanism"][
|
|
"handled"
|
|
],
|
|
True,
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["module"], None
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["type"], "Exception"
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["value"],
|
|
"test exception",
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][
|
|
"type"
|
|
],
|
|
"raw",
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][
|
|
"frames"
|
|
][0]["filename"],
|
|
"posthog/test/test_client.py",
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][
|
|
"frames"
|
|
][0]["function"],
|
|
"test_basic_capture_exception_with_no_exception_given",
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][
|
|
"frames"
|
|
][0]["module"],
|
|
"posthog.test.test_client",
|
|
)
|
|
self.assertEqual(
|
|
capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][
|
|
"frames"
|
|
][0]["in_app"],
|
|
True,
|
|
)
|
|
|
|
def test_basic_capture_exception_with_no_exception_happening(self):
|
|
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
|
|
with self.assertLogs("posthog", level="WARNING") as logs:
|
|
client = self.client
|
|
client.capture_exception(None)
|
|
|
|
self.assertFalse(patch_capture.called)
|
|
self.assertEqual(
|
|
logs.output[0],
|
|
"WARNING:posthog:No exception information available",
|
|
)
|
|
|
|
def test_capture_exception_logs_when_enabled(self):
|
|
client = Client(FAKE_TEST_API_KEY, log_captured_exceptions=True)
|
|
with self.assertLogs("posthog", level="ERROR") as logs:
|
|
client.capture_exception(
|
|
Exception("test exception"), distinct_id="distinct_id"
|
|
)
|
|
self.assertEqual(
|
|
logs.output[0], "ERROR:posthog:test exception\nNoneType: None"
|
|
)
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_basic_capture_with_feature_flags(self, patch_flags):
|
|
patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
msg_uuid = client.capture(
|
|
"python test event", distinct_id="distinct_id", send_feature_flags=True
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
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"]["$feature/beta-feature"], "random-variant"
|
|
)
|
|
self.assertEqual(
|
|
msg["properties"]["$active_feature_flags"], ["beta-feature"]
|
|
)
|
|
|
|
self.assertEqual(patch_flags.call_count, 1)
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_basic_capture_with_locally_evaluated_feature_flags(self, patch_flags):
|
|
patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
|
|
|
|
multivariate_flag = {
|
|
"id": 1,
|
|
"name": "Beta Feature",
|
|
"key": "beta-feature-local",
|
|
"active": True,
|
|
"rollout_percentage": 100,
|
|
"filters": {
|
|
"groups": [
|
|
{
|
|
"properties": [
|
|
{
|
|
"key": "email",
|
|
"type": "person",
|
|
"value": "test@posthog.com",
|
|
"operator": "exact",
|
|
}
|
|
],
|
|
"rollout_percentage": 100,
|
|
},
|
|
{
|
|
"rollout_percentage": 50,
|
|
},
|
|
],
|
|
"multivariate": {
|
|
"variants": [
|
|
{
|
|
"key": "first-variant",
|
|
"name": "First Variant",
|
|
"rollout_percentage": 50,
|
|
},
|
|
{
|
|
"key": "second-variant",
|
|
"name": "Second Variant",
|
|
"rollout_percentage": 25,
|
|
},
|
|
{
|
|
"key": "third-variant",
|
|
"name": "Third Variant",
|
|
"rollout_percentage": 25,
|
|
},
|
|
]
|
|
},
|
|
"payloads": {
|
|
"first-variant": "some-payload",
|
|
"third-variant": {"a": "json"},
|
|
},
|
|
},
|
|
}
|
|
basic_flag = {
|
|
"id": 1,
|
|
"name": "Beta Feature",
|
|
"key": "person-flag",
|
|
"active": True,
|
|
"filters": {
|
|
"groups": [
|
|
{
|
|
"properties": [
|
|
{
|
|
"key": "region",
|
|
"operator": "exact",
|
|
"value": ["USA"],
|
|
"type": "person",
|
|
}
|
|
],
|
|
"rollout_percentage": 100,
|
|
}
|
|
],
|
|
"payloads": {"true": 300},
|
|
},
|
|
}
|
|
false_flag = {
|
|
"id": 1,
|
|
"name": "Beta Feature",
|
|
"key": "false-flag",
|
|
"active": True,
|
|
"filters": {
|
|
"groups": [
|
|
{
|
|
"properties": [],
|
|
"rollout_percentage": 0,
|
|
}
|
|
],
|
|
"payloads": {"true": 300},
|
|
},
|
|
}
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
client.feature_flags = [multivariate_flag, basic_flag, false_flag]
|
|
|
|
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
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"]["$feature/beta-feature-local"], "third-variant"
|
|
)
|
|
self.assertEqual(msg["properties"]["$feature/false-flag"], False)
|
|
self.assertEqual(
|
|
msg["properties"]["$active_feature_flags"], ["beta-feature-local"]
|
|
)
|
|
assert "$feature/beta-feature" not in msg["properties"]
|
|
|
|
self.assertEqual(patch_flags.call_count, 0)
|
|
|
|
# test that flags are not evaluated without local evaluation
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
client.feature_flags = []
|
|
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
assert "$feature/beta-feature" not in msg["properties"]
|
|
assert "$feature/beta-feature-local" not in msg["properties"]
|
|
assert "$feature/false-flag" not in msg["properties"]
|
|
assert "$active_feature_flags" not in msg["properties"]
|
|
|
|
@mock.patch("posthog.client.get")
|
|
def test_load_feature_flags_quota_limited(self, patch_get):
|
|
mock_response = {
|
|
"type": "quota_limited",
|
|
"detail": "You have exceeded your feature flag request quota",
|
|
"code": "payment_required",
|
|
}
|
|
patch_get.side_effect = APIError(402, mock_response["detail"])
|
|
|
|
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
|
with self.assertLogs("posthog", level="WARNING") as logs:
|
|
client._load_feature_flags()
|
|
|
|
self.assertEqual(client.feature_flags, [])
|
|
self.assertEqual(client.feature_flags_by_key, {})
|
|
self.assertEqual(client.group_type_mapping, {})
|
|
self.assertEqual(client.cohorts, {})
|
|
self.assertIn("PostHog feature flags quota limited", logs.output[0])
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_dont_override_capture_with_local_flags(self, patch_flags):
|
|
patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
|
|
|
|
multivariate_flag = {
|
|
"id": 1,
|
|
"name": "Beta Feature",
|
|
"key": "beta-feature-local",
|
|
"active": True,
|
|
"rollout_percentage": 100,
|
|
"filters": {
|
|
"groups": [
|
|
{
|
|
"properties": [
|
|
{
|
|
"key": "email",
|
|
"type": "person",
|
|
"value": "test@posthog.com",
|
|
"operator": "exact",
|
|
}
|
|
],
|
|
"rollout_percentage": 100,
|
|
},
|
|
{
|
|
"rollout_percentage": 50,
|
|
},
|
|
],
|
|
"multivariate": {
|
|
"variants": [
|
|
{
|
|
"key": "first-variant",
|
|
"name": "First Variant",
|
|
"rollout_percentage": 50,
|
|
},
|
|
{
|
|
"key": "second-variant",
|
|
"name": "Second Variant",
|
|
"rollout_percentage": 25,
|
|
},
|
|
{
|
|
"key": "third-variant",
|
|
"name": "Third Variant",
|
|
"rollout_percentage": 25,
|
|
},
|
|
]
|
|
},
|
|
"payloads": {
|
|
"first-variant": "some-payload",
|
|
"third-variant": {"a": "json"},
|
|
},
|
|
},
|
|
}
|
|
basic_flag = {
|
|
"id": 1,
|
|
"name": "Beta Feature",
|
|
"key": "person-flag",
|
|
"active": True,
|
|
"filters": {
|
|
"groups": [
|
|
{
|
|
"properties": [
|
|
{
|
|
"key": "region",
|
|
"operator": "exact",
|
|
"value": ["USA"],
|
|
"type": "person",
|
|
}
|
|
],
|
|
"rollout_percentage": 100,
|
|
}
|
|
],
|
|
"payloads": {"true": 300},
|
|
},
|
|
}
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
client.feature_flags = [multivariate_flag, basic_flag]
|
|
|
|
msg_uuid = client.capture(
|
|
"python test event",
|
|
distinct_id="distinct_id",
|
|
properties={"$feature/beta-feature-local": "my-custom-variant"},
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
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"]["$feature/beta-feature-local"], "my-custom-variant"
|
|
)
|
|
self.assertEqual(
|
|
msg["properties"]["$active_feature_flags"], ["beta-feature-local"]
|
|
)
|
|
assert "$feature/beta-feature" not in msg["properties"]
|
|
assert "$feature/person-flag" not in msg["properties"]
|
|
|
|
self.assertEqual(patch_flags.call_count, 0)
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_basic_capture_with_feature_flags_returns_active_only(self, patch_flags):
|
|
patch_flags.return_value = {
|
|
"featureFlags": {
|
|
"beta-feature": "random-variant",
|
|
"alpha-feature": True,
|
|
"off-feature": False,
|
|
}
|
|
}
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
msg_uuid = client.capture(
|
|
"python test event", distinct_id="distinct_id", send_feature_flags=True
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
self.assertTrue(msg["properties"]["$geoip_disable"])
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
self.assertEqual(
|
|
msg["properties"]["$feature/beta-feature"], "random-variant"
|
|
)
|
|
self.assertEqual(msg["properties"]["$feature/alpha-feature"], True)
|
|
self.assertEqual(
|
|
msg["properties"]["$active_feature_flags"],
|
|
["beta-feature", "alpha-feature"],
|
|
)
|
|
|
|
self.assertEqual(patch_flags.call_count, 1)
|
|
patch_flags.assert_called_with(
|
|
"random_key",
|
|
"https://us.i.posthog.com",
|
|
timeout=3,
|
|
distinct_id="distinct_id",
|
|
groups={},
|
|
person_properties={},
|
|
group_properties={},
|
|
geoip_disable=True,
|
|
)
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_basic_capture_with_feature_flags_and_disable_geoip_returns_correctly(
|
|
self, patch_flags
|
|
):
|
|
patch_flags.return_value = {
|
|
"featureFlags": {
|
|
"beta-feature": "random-variant",
|
|
"alpha-feature": True,
|
|
"off-feature": False,
|
|
}
|
|
}
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
host="https://app.posthog.com",
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
disable_geoip=True,
|
|
feature_flags_request_timeout_seconds=12,
|
|
sync_mode=True,
|
|
)
|
|
msg_uuid = client.capture(
|
|
"python test event",
|
|
distinct_id="distinct_id",
|
|
send_feature_flags=True,
|
|
disable_geoip=False,
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
self.assertTrue("$geoip_disable" not in msg["properties"])
|
|
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"]["$feature/beta-feature"], "random-variant"
|
|
)
|
|
self.assertEqual(msg["properties"]["$feature/alpha-feature"], True)
|
|
self.assertEqual(
|
|
msg["properties"]["$active_feature_flags"],
|
|
["beta-feature", "alpha-feature"],
|
|
)
|
|
|
|
self.assertEqual(patch_flags.call_count, 1)
|
|
patch_flags.assert_called_with(
|
|
"random_key",
|
|
"https://us.i.posthog.com",
|
|
timeout=12,
|
|
distinct_id="distinct_id",
|
|
groups={},
|
|
person_properties={},
|
|
group_properties={},
|
|
geoip_disable=False,
|
|
)
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_basic_capture_with_feature_flags_switched_off_doesnt_send_them(
|
|
self, patch_flags
|
|
):
|
|
patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
msg_uuid = client.capture(
|
|
"python test event", distinct_id="distinct_id", send_feature_flags=False
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
self.assertTrue("$feature/beta-feature" not in msg["properties"])
|
|
self.assertTrue("$active_feature_flags" not in msg["properties"])
|
|
|
|
self.assertEqual(patch_flags.call_count, 0)
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_capture_with_send_feature_flags_options_only_evaluate_locally_true(
|
|
self, patch_flags
|
|
):
|
|
"""Test that SendFeatureFlagsOptions with only_evaluate_locally=True uses local evaluation"""
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
|
|
# Set up local flags
|
|
client.feature_flags = [
|
|
{
|
|
"id": 1,
|
|
"key": "local-flag",
|
|
"active": True,
|
|
"filters": {
|
|
"groups": [
|
|
{
|
|
"properties": [{"key": "region", "value": "US"}],
|
|
"rollout_percentage": 100,
|
|
}
|
|
],
|
|
},
|
|
}
|
|
]
|
|
|
|
send_options = {
|
|
"only_evaluate_locally": True,
|
|
"person_properties": {"region": "US"},
|
|
}
|
|
|
|
msg_uuid = client.capture(
|
|
"test event", distinct_id="distinct_id", send_feature_flags=send_options
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Verify flags() was not called (no remote evaluation)
|
|
patch_flags.assert_not_called()
|
|
|
|
# Check the message includes the local flag
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["properties"]["$feature/local-flag"], True)
|
|
self.assertEqual(msg["properties"]["$active_feature_flags"], ["local-flag"])
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_capture_with_send_feature_flags_options_only_evaluate_locally_false(
|
|
self, patch_flags
|
|
):
|
|
"""Test that SendFeatureFlagsOptions with only_evaluate_locally=False forces remote evaluation"""
|
|
patch_flags.return_value = {"featureFlags": {"remote-flag": "remote-value"}}
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
|
|
send_options = {
|
|
"only_evaluate_locally": False,
|
|
"person_properties": {"plan": "premium"},
|
|
"group_properties": {"company": {"type": "enterprise"}},
|
|
}
|
|
|
|
msg_uuid = client.capture(
|
|
"test event",
|
|
distinct_id="distinct_id",
|
|
groups={"company": "acme"},
|
|
send_feature_flags=send_options,
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Verify flags() was called with the correct properties
|
|
patch_flags.assert_called_once()
|
|
call_args = patch_flags.call_args[1]
|
|
self.assertEqual(call_args["person_properties"], {"plan": "premium"})
|
|
self.assertEqual(
|
|
call_args["group_properties"], {"company": {"type": "enterprise"}}
|
|
)
|
|
|
|
# Check the message includes the remote flag
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["properties"]["$feature/remote-flag"], "remote-value")
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_capture_with_send_feature_flags_options_default_behavior(
|
|
self, patch_flags
|
|
):
|
|
"""Test that SendFeatureFlagsOptions without only_evaluate_locally defaults to remote evaluation"""
|
|
patch_flags.return_value = {"featureFlags": {"default-flag": "default-value"}}
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
|
|
send_options = {
|
|
"person_properties": {"subscription": "pro"},
|
|
}
|
|
|
|
msg_uuid = client.capture(
|
|
"test event", distinct_id="distinct_id", send_feature_flags=send_options
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Verify flags() was called (default to remote evaluation)
|
|
patch_flags.assert_called_once()
|
|
call_args = patch_flags.call_args[1]
|
|
self.assertEqual(call_args["person_properties"], {"subscription": "pro"})
|
|
|
|
# Check the message includes the flag
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(
|
|
msg["properties"]["$feature/default-flag"], "default-value"
|
|
)
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_capture_exception_with_send_feature_flags_options(self, patch_flags):
|
|
"""Test that capture_exception also supports SendFeatureFlagsOptions"""
|
|
patch_flags.return_value = {"featureFlags": {"exception-flag": True}}
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
personal_api_key=FAKE_TEST_API_KEY,
|
|
sync_mode=True,
|
|
)
|
|
|
|
send_options = {
|
|
"only_evaluate_locally": False,
|
|
"person_properties": {"user_type": "admin"},
|
|
}
|
|
|
|
try:
|
|
raise ValueError("Test exception")
|
|
except ValueError as e:
|
|
msg_uuid = client.capture_exception(
|
|
e, distinct_id="distinct_id", send_feature_flags=send_options
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Verify flags() was called with the correct properties
|
|
patch_flags.assert_called_once()
|
|
call_args = patch_flags.call_args[1]
|
|
self.assertEqual(call_args["person_properties"], {"user_type": "admin"})
|
|
|
|
# Check the message includes the flag
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "$exception")
|
|
self.assertEqual(msg["properties"]["$feature/exception-flag"], True)
|
|
|
|
def test_stringifies_distinct_id(self):
|
|
# A large number that loses precision in node:
|
|
# node -e "console.log(157963456373623802 + 1)" > 157963456373623800
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
|
msg_uuid = client.capture(
|
|
"python test event", distinct_id=157963456373623802
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["distinct_id"], "157963456373623802")
|
|
|
|
def test_advanced_capture(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)
|
|
msg_uuid = client.capture(
|
|
"python test event",
|
|
distinct_id="distinct_id",
|
|
properties={"property": "value"},
|
|
timestamp=datetime(2014, 9, 3),
|
|
uuid="new-uuid",
|
|
)
|
|
|
|
self.assertEqual(msg_uuid, "new-uuid")
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
|
self.assertEqual(msg["properties"]["property"], "value")
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
self.assertEqual(msg["uuid"], "new-uuid")
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
self.assertTrue("$groups" not in msg["properties"])
|
|
|
|
def test_groups_capture(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)
|
|
msg_uuid = client.capture(
|
|
"test_event",
|
|
distinct_id="distinct_id",
|
|
groups={"company": "id:5", "instance": "app.posthog.com"},
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(
|
|
msg["properties"]["$groups"],
|
|
{"company": "id:5", "instance": "app.posthog.com"},
|
|
)
|
|
|
|
def test_basic_set(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)
|
|
msg_uuid = client.set(
|
|
distinct_id="distinct_id", properties={"trait": "value"}
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["$set"]["trait"], "value")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
|
|
def test_advanced_set(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)
|
|
msg_uuid = client.set(
|
|
distinct_id="distinct_id",
|
|
properties={"trait": "value"},
|
|
timestamp=datetime(2014, 9, 3),
|
|
uuid="new-uuid",
|
|
)
|
|
|
|
self.assertEqual(msg_uuid, "new-uuid")
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
|
self.assertEqual(msg["$set"]["trait"], "value")
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertEqual(msg["uuid"], "new-uuid")
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
|
|
def test_basic_set_once(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)
|
|
msg_uuid = client.set_once(
|
|
distinct_id="distinct_id", properties={"trait": "value"}
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["$set_once"]["trait"], "value")
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
|
|
def test_advanced_set_once(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)
|
|
msg_uuid = client.set_once(
|
|
distinct_id="distinct_id",
|
|
properties={"trait": "value"},
|
|
timestamp=datetime(2014, 9, 3),
|
|
uuid="new-uuid",
|
|
)
|
|
|
|
self.assertEqual(msg_uuid, "new-uuid")
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
|
self.assertEqual(msg["$set_once"]["trait"], "value")
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertEqual(msg["uuid"], "new-uuid")
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
|
|
def test_basic_group_identify(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)
|
|
msg_uuid = client.group_identify("organization", "id:5")
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "$groupidentify")
|
|
self.assertEqual(
|
|
msg["properties"],
|
|
{
|
|
"$group_type": "organization",
|
|
"$group_key": "id:5",
|
|
"$group_set": {},
|
|
"$lib": "posthog-python",
|
|
"$lib_version": VERSION,
|
|
"$geoip_disable": True,
|
|
},
|
|
)
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
|
|
def test_basic_group_identify_with_distinct_id(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)
|
|
msg_uuid = client.group_identify(
|
|
"organization", "id:5", distinct_id="distinct_id"
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "$groupidentify")
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
self.assertEqual(
|
|
msg["properties"],
|
|
{
|
|
"$group_type": "organization",
|
|
"$group_key": "id:5",
|
|
"$group_set": {},
|
|
"$lib": "posthog-python",
|
|
"$lib_version": VERSION,
|
|
"$geoip_disable": True,
|
|
},
|
|
)
|
|
self.assertTrue(isinstance(msg["timestamp"], str))
|
|
self.assertIsNotNone(msg.get("uuid"))
|
|
|
|
def test_advanced_group_identify(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)
|
|
msg_uuid = client.group_identify(
|
|
"organization",
|
|
"id:5",
|
|
{"trait": "value"},
|
|
timestamp=datetime(2014, 9, 3),
|
|
uuid="new-uuid",
|
|
)
|
|
|
|
self.assertEqual(msg_uuid, "new-uuid")
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "$groupidentify")
|
|
self.assertEqual(
|
|
msg["properties"],
|
|
{
|
|
"$group_type": "organization",
|
|
"$group_key": "id:5",
|
|
"$group_set": {"trait": "value"},
|
|
"$lib": "posthog-python",
|
|
"$lib_version": VERSION,
|
|
"$geoip_disable": True,
|
|
},
|
|
)
|
|
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
|
|
|
def test_advanced_group_identify_with_distinct_id(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)
|
|
msg_uuid = client.group_identify(
|
|
"organization",
|
|
"id:5",
|
|
{"trait": "value"},
|
|
timestamp=datetime(2014, 9, 3),
|
|
uuid="new-uuid",
|
|
distinct_id="distinct_id",
|
|
)
|
|
|
|
self.assertEqual(msg_uuid, "new-uuid")
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "$groupidentify")
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
|
|
self.assertEqual(
|
|
msg["properties"],
|
|
{
|
|
"$group_type": "organization",
|
|
"$group_key": "id:5",
|
|
"$group_set": {"trait": "value"},
|
|
"$lib": "posthog-python",
|
|
"$lib_version": VERSION,
|
|
"$geoip_disable": True,
|
|
},
|
|
)
|
|
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
|
|
|
def test_basic_alias(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)
|
|
msg_uuid = client.alias("previousId", "distinct_id")
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
self.assertEqual(msg["properties"]["distinct_id"], "previousId")
|
|
self.assertEqual(msg["properties"]["alias"], "distinct_id")
|
|
|
|
@parameterized.expand(
|
|
[
|
|
# test_name, session_id, additional_properties, expected_properties
|
|
("basic_session_id", "test-session-123", {}, {}),
|
|
(
|
|
"session_id_with_other_properties",
|
|
"test-session-456",
|
|
{
|
|
"custom_prop": "custom_value",
|
|
"$process_person_profile": False,
|
|
"$current_url": "https://example.com",
|
|
},
|
|
{
|
|
"custom_prop": "custom_value",
|
|
"$process_person_profile": False,
|
|
"$current_url": "https://example.com",
|
|
},
|
|
),
|
|
("session_id_uuid_format", str(uuid4()), {}, {}),
|
|
("session_id_numeric_string", "1234567890", {}, {}),
|
|
("session_id_empty_string", "", {}, {}),
|
|
("session_id_with_special_chars", "session-123_test.id", {}, {}),
|
|
]
|
|
)
|
|
def test_capture_with_session_id_variations(
|
|
self, test_name, session_id, additional_properties, expected_properties
|
|
):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
|
|
|
properties = {"$session_id": session_id, **additional_properties}
|
|
msg_uuid = client.capture(
|
|
"python test event", distinct_id="distinct_id", properties=properties
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], "python test event")
|
|
self.assertEqual(msg["distinct_id"], "distinct_id")
|
|
self.assertEqual(msg["properties"]["$session_id"], session_id)
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
|
|
# Check additional expected properties
|
|
for key, value in expected_properties.items():
|
|
self.assertEqual(msg["properties"][key], value)
|
|
|
|
def test_session_id_preserved_with_groups(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)
|
|
session_id = "group-session-101"
|
|
|
|
msg_uuid = client.capture(
|
|
"test_event",
|
|
distinct_id="distinct_id",
|
|
properties={"$session_id": session_id},
|
|
groups={"company": "id:5", "instance": "app.posthog.com"},
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["properties"]["$session_id"], session_id)
|
|
self.assertEqual(
|
|
msg["properties"]["$groups"],
|
|
{"company": "id:5", "instance": "app.posthog.com"},
|
|
)
|
|
|
|
def test_session_id_with_anonymous_event(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)
|
|
session_id = "anonymous-session-202"
|
|
|
|
msg_uuid = client.capture(
|
|
"anonymous_event",
|
|
distinct_id="distinct_id",
|
|
properties={
|
|
"$session_id": session_id,
|
|
"$process_person_profile": False,
|
|
},
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["properties"]["$session_id"], session_id)
|
|
self.assertEqual(msg["properties"]["$process_person_profile"], False)
|
|
|
|
@parameterized.expand(
|
|
[
|
|
# test_name, event_name, session_id, additional_properties, expected_additional_properties
|
|
(
|
|
"screen_event",
|
|
"$screen",
|
|
"special-session-505",
|
|
{"$screen_name": "HomeScreen"},
|
|
{"$screen_name": "HomeScreen"},
|
|
),
|
|
(
|
|
"survey_event",
|
|
"survey sent",
|
|
"survey-session-606",
|
|
{
|
|
"$survey_id": "survey_123",
|
|
"$survey_questions": [
|
|
{"id": "q1", "question": "How likely are you to recommend us?"}
|
|
],
|
|
},
|
|
{"$survey_id": "survey_123"},
|
|
),
|
|
(
|
|
"complex_properties_event",
|
|
"complex_event",
|
|
"mixed-session-707",
|
|
{
|
|
"$current_url": "https://example.com/page",
|
|
"$process_person_profile": True,
|
|
"custom_property": "custom_value",
|
|
"numeric_property": 42,
|
|
"boolean_property": True,
|
|
},
|
|
{
|
|
"$current_url": "https://example.com/page",
|
|
"$process_person_profile": True,
|
|
"custom_property": "custom_value",
|
|
"numeric_property": 42,
|
|
"boolean_property": True,
|
|
},
|
|
),
|
|
(
|
|
"csp_violation",
|
|
"$csp_violation",
|
|
"csp-session-789",
|
|
{
|
|
"$csp_version": "1.0",
|
|
"$current_url": "https://example.com/page",
|
|
"$process_person_profile": False,
|
|
"$raw_user_agent": "Mozilla/5.0 Test Agent",
|
|
"$csp_document_url": "https://example.com/page",
|
|
"$csp_blocked_url": "https://malicious.com/script.js",
|
|
"$csp_violated_directive": "script-src",
|
|
},
|
|
{
|
|
"$csp_version": "1.0",
|
|
"$current_url": "https://example.com/page",
|
|
"$process_person_profile": False,
|
|
"$raw_user_agent": "Mozilla/5.0 Test Agent",
|
|
"$csp_document_url": "https://example.com/page",
|
|
"$csp_blocked_url": "https://malicious.com/script.js",
|
|
"$csp_violated_directive": "script-src",
|
|
},
|
|
),
|
|
]
|
|
)
|
|
def test_session_id_with_different_event_types(
|
|
self,
|
|
test_name,
|
|
event_name,
|
|
session_id,
|
|
additional_properties,
|
|
expected_additional_properties,
|
|
):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
|
|
|
properties = {"$session_id": session_id, **additional_properties}
|
|
msg_uuid = client.capture(
|
|
event_name, distinct_id="distinct_id", properties=properties
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["event"], event_name)
|
|
self.assertEqual(msg["properties"]["$session_id"], session_id)
|
|
|
|
# Check additional expected properties
|
|
for key, value in expected_additional_properties.items():
|
|
self.assertEqual(msg["properties"][key], value)
|
|
|
|
# Verify system properties are still added
|
|
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
|
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
|
|
|
@parameterized.expand(
|
|
[
|
|
# test_name, super_properties, event_session_id, expected_session_id, expected_super_props
|
|
(
|
|
"super_properties_override_session_id",
|
|
{"$session_id": "super-session", "source": "test"},
|
|
"event-session-808",
|
|
"super-session",
|
|
{"source": "test"},
|
|
),
|
|
(
|
|
"no_super_properties_conflict",
|
|
{"source": "test", "version": "1.0"},
|
|
"event-session-909",
|
|
"event-session-909",
|
|
{"source": "test", "version": "1.0"},
|
|
),
|
|
(
|
|
"empty_super_properties",
|
|
{},
|
|
"event-session-111",
|
|
"event-session-111",
|
|
{},
|
|
),
|
|
(
|
|
"super_properties_with_other_dollar_props",
|
|
{"$current_url": "https://super.com", "source": "test"},
|
|
"event-session-222",
|
|
"event-session-222",
|
|
{"$current_url": "https://super.com", "source": "test"},
|
|
),
|
|
]
|
|
)
|
|
def test_session_id_with_super_properties_variations(
|
|
self,
|
|
test_name,
|
|
super_properties,
|
|
event_session_id,
|
|
expected_session_id,
|
|
expected_super_props,
|
|
):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY, super_properties=super_properties, sync_mode=True
|
|
)
|
|
|
|
msg_uuid = client.capture(
|
|
"test_event",
|
|
distinct_id="distinct_id",
|
|
properties={"$session_id": event_session_id},
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(msg["properties"]["$session_id"], expected_session_id)
|
|
|
|
# Check expected super properties are present
|
|
for key, value in expected_super_props.items():
|
|
self.assertEqual(msg["properties"][key], value)
|
|
|
|
def test_flush(self):
|
|
client = self.client
|
|
# set up the consumer with more requests than a single batch will allow
|
|
for i in range(1000):
|
|
client.capture(
|
|
"event", distinct_id="distinct_id", properties={"trait": "value"}
|
|
)
|
|
# We can't reliably assert that the queue is non-empty here; that's
|
|
# a race condition. We do our best to load it up though.
|
|
client.flush()
|
|
# Make sure that the client queue is empty after flushing
|
|
self.assertTrue(client.queue.empty())
|
|
|
|
def test_shutdown(self):
|
|
client = self.client
|
|
# set up the consumer with more requests than a single batch will allow
|
|
for i in range(1000):
|
|
client.capture(
|
|
"test event", distinct_id="distinct_id", properties={"trait": "value"}
|
|
)
|
|
client.shutdown()
|
|
# we expect two things after shutdown:
|
|
# 1. client queue is empty
|
|
# 2. consumer thread has stopped
|
|
self.assertTrue(client.queue.empty())
|
|
for consumer in client.consumers:
|
|
self.assertFalse(consumer.is_alive())
|
|
|
|
def test_synchronous(self):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(FAKE_TEST_API_KEY, sync_mode=True)
|
|
|
|
msg_uuid = client.capture("test event", distinct_id="distinct_id")
|
|
self.assertFalse(client.consumers)
|
|
self.assertTrue(client.queue.empty())
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Verify the message was sent immediately
|
|
mock_post.assert_called_once()
|
|
|
|
def test_overflow(self):
|
|
client = Client(FAKE_TEST_API_KEY, max_queue_size=1)
|
|
# Ensure consumer thread is no longer uploading
|
|
client.join()
|
|
|
|
for i in range(10):
|
|
client.capture("test event", distinct_id="distinct_id")
|
|
|
|
msg_uuid = client.capture("test event", distinct_id="distinct_id")
|
|
# Make sure we are informed that the queue is at capacity
|
|
self.assertIsNone(msg_uuid)
|
|
|
|
def test_unicode(self):
|
|
Client(six.u("unicode_key"))
|
|
|
|
def test_numeric_distinct_id(self):
|
|
self.client.capture("python event", distinct_id=1234)
|
|
self.client.flush()
|
|
self.assertFalse(self.failed)
|
|
|
|
def test_debug(self):
|
|
Client("bad_key", debug=True)
|
|
|
|
def test_gzip(self):
|
|
client = Client(FAKE_TEST_API_KEY, on_error=self.fail, gzip=True)
|
|
for _ in range(10):
|
|
client.capture(
|
|
"event", distinct_id="distinct_id", properties={"trait": "value"}
|
|
)
|
|
client.flush()
|
|
self.assertFalse(self.failed)
|
|
|
|
def test_user_defined_flush_at(self):
|
|
client = Client(
|
|
FAKE_TEST_API_KEY, on_error=self.fail, flush_at=10, flush_interval=3
|
|
)
|
|
|
|
def mock_post_fn(*args, **kwargs):
|
|
self.assertEqual(len(kwargs["batch"]), 10)
|
|
|
|
# the post function should be called 2 times, with a batch size of 10
|
|
# each time.
|
|
with mock.patch(
|
|
"posthog.consumer.batch_post", side_effect=mock_post_fn
|
|
) as mock_post:
|
|
for _ in range(20):
|
|
client.capture(
|
|
"event", distinct_id="distinct_id", properties={"trait": "value"}
|
|
)
|
|
time.sleep(1)
|
|
self.assertEqual(mock_post.call_count, 2)
|
|
|
|
def test_user_defined_timeout(self):
|
|
client = Client(FAKE_TEST_API_KEY, timeout=10)
|
|
for consumer in client.consumers:
|
|
self.assertEqual(consumer.timeout, 10)
|
|
|
|
def test_default_timeout_15(self):
|
|
client = Client(FAKE_TEST_API_KEY)
|
|
for consumer in client.consumers:
|
|
self.assertEqual(consumer.timeout, 15)
|
|
|
|
def test_disabled(self):
|
|
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, disabled=True)
|
|
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
|
client.flush()
|
|
self.assertIsNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_disabled_with_feature_flags(self, patch_flags):
|
|
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, disabled=True)
|
|
|
|
response = client.get_feature_flag("beta-feature", "12345")
|
|
self.assertIsNone(response)
|
|
patch_flags.assert_not_called()
|
|
|
|
response = client.feature_enabled("beta-feature", "12345")
|
|
self.assertIsNone(response)
|
|
patch_flags.assert_not_called()
|
|
|
|
response = client.get_all_flags("12345")
|
|
self.assertIsNone(response)
|
|
patch_flags.assert_not_called()
|
|
|
|
response = client.get_feature_flag_payload("key", "12345")
|
|
self.assertIsNone(response)
|
|
patch_flags.assert_not_called()
|
|
|
|
response = client.get_all_flags_and_payloads("12345")
|
|
self.assertEqual(response, {"featureFlags": None, "featureFlagPayloads": None})
|
|
patch_flags.assert_not_called()
|
|
|
|
# no capture calls
|
|
self.assertTrue(client.queue.empty())
|
|
|
|
def test_enabled_to_disabled(self):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
disabled=False,
|
|
sync_mode=True,
|
|
)
|
|
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
self.assertEqual(msg["event"], "python test event")
|
|
|
|
client.disabled = True
|
|
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
|
self.assertIsNone(msg_uuid)
|
|
self.assertFalse(self.failed)
|
|
|
|
def test_disable_geoip_default_on_events(self):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
disable_geoip=True,
|
|
sync_mode=True,
|
|
)
|
|
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
capture_msg = batch_data[0]
|
|
self.assertEqual(capture_msg["properties"]["$geoip_disable"], True)
|
|
|
|
def test_disable_geoip_override_on_events(self):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
disable_geoip=False,
|
|
sync_mode=True,
|
|
)
|
|
msg_uuid = client.set(
|
|
distinct_id="distinct_id",
|
|
properties={"a": "b", "c": "d"},
|
|
disable_geoip=True,
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
msg_uuid = client.capture(
|
|
"event",
|
|
distinct_id="distinct_id",
|
|
properties={"trait": "value"},
|
|
disable_geoip=False,
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Check both calls were made
|
|
self.assertEqual(mock_post.call_count, 2)
|
|
|
|
# Check set event
|
|
set_batch = mock_post.call_args_list[0][1]["batch"]
|
|
capture_msg = set_batch[0]
|
|
self.assertEqual(capture_msg["properties"]["$geoip_disable"], True)
|
|
|
|
# Check page event
|
|
page_batch = mock_post.call_args_list[1][1]["batch"]
|
|
identify_msg = page_batch[0]
|
|
self.assertEqual("$geoip_disable" not in identify_msg["properties"], True)
|
|
|
|
def test_disable_geoip_method_overrides_init_on_events(self):
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
on_error=self.set_fail,
|
|
disable_geoip=True,
|
|
sync_mode=True,
|
|
)
|
|
msg_uuid = client.capture(
|
|
"python test event", distinct_id="distinct_id", disable_geoip=False
|
|
)
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
self.assertTrue("$geoip_disable" not in msg["properties"])
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_disable_geoip_default_on_decide(self, patch_flags):
|
|
patch_flags.return_value = {
|
|
"featureFlags": {
|
|
"beta-feature": "random-variant",
|
|
"alpha-feature": True,
|
|
"off-feature": False,
|
|
}
|
|
}
|
|
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, disable_geoip=False)
|
|
client.get_feature_flag("random_key", "some_id", disable_geoip=True)
|
|
patch_flags.assert_called_with(
|
|
"random_key",
|
|
"https://us.i.posthog.com",
|
|
timeout=3,
|
|
distinct_id="some_id",
|
|
groups={},
|
|
person_properties={"distinct_id": "some_id"},
|
|
group_properties={},
|
|
geoip_disable=True,
|
|
)
|
|
patch_flags.reset_mock()
|
|
client.feature_enabled(
|
|
"random_key", "feature_enabled_distinct_id", disable_geoip=True
|
|
)
|
|
patch_flags.assert_called_with(
|
|
"random_key",
|
|
"https://us.i.posthog.com",
|
|
timeout=3,
|
|
distinct_id="feature_enabled_distinct_id",
|
|
groups={},
|
|
person_properties={"distinct_id": "feature_enabled_distinct_id"},
|
|
group_properties={},
|
|
geoip_disable=True,
|
|
)
|
|
patch_flags.reset_mock()
|
|
client.get_all_flags_and_payloads("all_flags_payloads_id")
|
|
patch_flags.assert_called_with(
|
|
"random_key",
|
|
"https://us.i.posthog.com",
|
|
timeout=3,
|
|
distinct_id="all_flags_payloads_id",
|
|
groups={},
|
|
person_properties={"distinct_id": "all_flags_payloads_id"},
|
|
group_properties={},
|
|
geoip_disable=False,
|
|
)
|
|
|
|
@mock.patch("posthog.client.Poller")
|
|
@mock.patch("posthog.client.get")
|
|
def test_call_identify_fails(self, patch_get, patch_poller):
|
|
def raise_effect():
|
|
raise Exception("http exception")
|
|
|
|
patch_get.return_value.raiseError.side_effect = raise_effect
|
|
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
|
client.feature_flags = [{"key": "example"}]
|
|
|
|
self.assertFalse(client.feature_enabled("example", "distinct_id"))
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_default_properties_get_added_properly(self, patch_flags):
|
|
patch_flags.return_value = {
|
|
"featureFlags": {
|
|
"beta-feature": "random-variant",
|
|
"alpha-feature": True,
|
|
"off-feature": False,
|
|
}
|
|
}
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
host="http://app2.posthog.com",
|
|
on_error=self.set_fail,
|
|
disable_geoip=False,
|
|
)
|
|
client.get_feature_flag(
|
|
"random_key",
|
|
"some_id",
|
|
groups={"company": "id:5", "instance": "app.posthog.com"},
|
|
person_properties={"x1": "y1"},
|
|
group_properties={"company": {"x": "y"}},
|
|
)
|
|
patch_flags.assert_called_with(
|
|
"random_key",
|
|
"http://app2.posthog.com",
|
|
timeout=3,
|
|
distinct_id="some_id",
|
|
groups={"company": "id:5", "instance": "app.posthog.com"},
|
|
person_properties={"distinct_id": "some_id", "x1": "y1"},
|
|
group_properties={
|
|
"company": {"$group_key": "id:5", "x": "y"},
|
|
"instance": {"$group_key": "app.posthog.com"},
|
|
},
|
|
geoip_disable=False,
|
|
)
|
|
|
|
patch_flags.reset_mock()
|
|
client.get_feature_flag(
|
|
"random_key",
|
|
"some_id",
|
|
groups={"company": "id:5", "instance": "app.posthog.com"},
|
|
person_properties={"distinct_id": "override"},
|
|
group_properties={
|
|
"company": {
|
|
"$group_key": "group_override",
|
|
}
|
|
},
|
|
)
|
|
patch_flags.assert_called_with(
|
|
"random_key",
|
|
"http://app2.posthog.com",
|
|
timeout=3,
|
|
distinct_id="some_id",
|
|
groups={"company": "id:5", "instance": "app.posthog.com"},
|
|
person_properties={"distinct_id": "override"},
|
|
group_properties={
|
|
"company": {"$group_key": "group_override"},
|
|
"instance": {"$group_key": "app.posthog.com"},
|
|
},
|
|
geoip_disable=False,
|
|
)
|
|
|
|
patch_flags.reset_mock()
|
|
# test nones
|
|
client.get_all_flags_and_payloads(
|
|
"some_id", groups={}, person_properties=None, group_properties=None
|
|
)
|
|
patch_flags.assert_called_with(
|
|
"random_key",
|
|
"http://app2.posthog.com",
|
|
timeout=3,
|
|
distinct_id="some_id",
|
|
groups={},
|
|
person_properties={"distinct_id": "some_id"},
|
|
group_properties={},
|
|
geoip_disable=False,
|
|
)
|
|
|
|
@parameterized.expand(
|
|
[
|
|
# name, sys_platform, version_info, expected_runtime, expected_version, expected_os, expected_os_version, platform_method, platform_return, distro_info
|
|
(
|
|
"macOS",
|
|
"darwin",
|
|
(3, 8, 10),
|
|
"MockPython",
|
|
"3.8.10",
|
|
"Mac OS X",
|
|
"10.15.7",
|
|
"mac_ver",
|
|
("10.15.7", "", ""),
|
|
None,
|
|
),
|
|
(
|
|
"Windows",
|
|
"win32",
|
|
(3, 8, 10),
|
|
"MockPython",
|
|
"3.8.10",
|
|
"Windows",
|
|
"10",
|
|
"win32_ver",
|
|
("10", "", "", ""),
|
|
None,
|
|
),
|
|
(
|
|
"Linux",
|
|
"linux",
|
|
(3, 8, 10),
|
|
"MockPython",
|
|
"3.8.10",
|
|
"Linux",
|
|
"20.04",
|
|
None,
|
|
None,
|
|
{"version": "20.04"},
|
|
),
|
|
]
|
|
)
|
|
def test_mock_system_context(
|
|
self,
|
|
_name,
|
|
sys_platform,
|
|
version_info,
|
|
expected_runtime,
|
|
expected_version,
|
|
expected_os,
|
|
expected_os_version,
|
|
platform_method,
|
|
platform_return,
|
|
distro_info,
|
|
):
|
|
"""Test that we can mock platform and sys for testing system_context"""
|
|
with mock.patch("posthog.utils.platform") as mock_platform:
|
|
with mock.patch("posthog.utils.sys") as mock_sys:
|
|
# Set up common mocks
|
|
mock_platform.python_implementation.return_value = expected_runtime
|
|
mock_sys.version_info = version_info
|
|
mock_sys.platform = sys_platform
|
|
|
|
# Set up platform-specific mocks
|
|
if platform_method:
|
|
getattr(
|
|
mock_platform, platform_method
|
|
).return_value = platform_return
|
|
|
|
# Special handling for Linux which uses distro module
|
|
if sys_platform == "linux":
|
|
# Directly patch the get_os_info function to return our expected values
|
|
with mock.patch(
|
|
"posthog.utils.get_os_info",
|
|
return_value=(expected_os, expected_os_version),
|
|
):
|
|
from posthog.utils import system_context
|
|
|
|
context = system_context()
|
|
else:
|
|
# Get system context for non-Linux platforms
|
|
from posthog.utils import system_context
|
|
|
|
context = system_context()
|
|
|
|
# Verify results
|
|
expected_context = {
|
|
"$python_runtime": expected_runtime,
|
|
"$python_version": expected_version,
|
|
"$os": expected_os,
|
|
"$os_version": expected_os_version,
|
|
}
|
|
|
|
assert context == expected_context
|
|
|
|
@mock.patch("posthog.client.flags")
|
|
def test_get_decide_returns_normalized_decide_response(self, patch_flags):
|
|
patch_flags.return_value = {
|
|
"featureFlags": {
|
|
"beta-feature": "random-variant",
|
|
"alpha-feature": True,
|
|
"off-feature": False,
|
|
},
|
|
"featureFlagPayloads": {"beta-feature": '{"some": "data"}'},
|
|
"errorsWhileComputingFlags": False,
|
|
"requestId": "test-id",
|
|
}
|
|
|
|
client = Client(FAKE_TEST_API_KEY)
|
|
distinct_id = "test_distinct_id"
|
|
groups = {"test_group_type": "test_group_id"}
|
|
person_properties = {"test_property": "test_value"}
|
|
|
|
response = client.get_flags_decision(distinct_id, groups, person_properties)
|
|
|
|
assert response == {
|
|
"flags": {
|
|
"beta-feature": FeatureFlag(
|
|
key="beta-feature",
|
|
enabled=True,
|
|
variant="random-variant",
|
|
reason=None,
|
|
metadata=LegacyFlagMetadata(
|
|
payload='{"some": "data"}',
|
|
),
|
|
),
|
|
"alpha-feature": FeatureFlag(
|
|
key="alpha-feature",
|
|
enabled=True,
|
|
variant=None,
|
|
reason=None,
|
|
metadata=LegacyFlagMetadata(
|
|
payload=None,
|
|
),
|
|
),
|
|
"off-feature": FeatureFlag(
|
|
key="off-feature",
|
|
enabled=False,
|
|
variant=None,
|
|
reason=None,
|
|
metadata=LegacyFlagMetadata(
|
|
payload=None,
|
|
),
|
|
),
|
|
},
|
|
"errorsWhileComputingFlags": False,
|
|
"requestId": "test-id",
|
|
}
|
|
|
|
def test_set_context_session_with_capture(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)
|
|
with new_context():
|
|
set_context_session("context-session-123")
|
|
|
|
msg_uuid = client.capture(
|
|
"test_event",
|
|
distinct_id="distinct_id",
|
|
properties={"custom_prop": "value"},
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(
|
|
msg["properties"]["$session_id"], "context-session-123"
|
|
)
|
|
|
|
def test_set_context_session_with_page_explicit_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)
|
|
with new_context():
|
|
set_context_session("page-explicit-session-789")
|
|
|
|
properties = {
|
|
"$session_id": get_context_session_id(),
|
|
"page_type": "landing",
|
|
}
|
|
msg_uuid = client.capture(
|
|
"$page", distinct_id="distinct_id", properties=properties
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(
|
|
msg["properties"]["$session_id"], "page-explicit-session-789"
|
|
)
|
|
|
|
def test_set_context_session_override_in_capture(self):
|
|
"""Test that explicit session ID overrides context session ID in capture"""
|
|
from posthog.contexts import new_context, set_context_session
|
|
|
|
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
|
with new_context():
|
|
set_context_session("context-session-override")
|
|
|
|
msg_uuid = client.capture(
|
|
"test_event",
|
|
distinct_id="distinct_id",
|
|
properties={
|
|
"$session_id": "explicit-session-override",
|
|
"custom_prop": "value",
|
|
},
|
|
)
|
|
|
|
self.assertIsNotNone(msg_uuid)
|
|
|
|
# Get the enqueued message from the mock
|
|
mock_post.assert_called_once()
|
|
batch_data = mock_post.call_args[1]["batch"]
|
|
msg = batch_data[0]
|
|
|
|
self.assertEqual(
|
|
msg["properties"]["$session_id"], "explicit-session-override"
|
|
)
|
|
|
|
@mock.patch("posthog.client.Poller")
|
|
@mock.patch("posthog.client.get")
|
|
def test_enable_local_evaluation_false_disables_poller(
|
|
self, patch_get, patch_poller
|
|
):
|
|
"""Test that when enable_local_evaluation=False, the poller is not started"""
|
|
patch_get.return_value = {
|
|
"flags": [
|
|
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True}
|
|
],
|
|
"group_type_mapping": {},
|
|
"cohorts": {},
|
|
}
|
|
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
personal_api_key="test-personal-key",
|
|
enable_local_evaluation=False,
|
|
)
|
|
|
|
# Load feature flags should not start the poller
|
|
client.load_feature_flags()
|
|
|
|
# Assert that the poller was not created/started
|
|
patch_poller.assert_not_called()
|
|
# But the feature flags should still be loaded
|
|
patch_get.assert_called_once()
|
|
self.assertEqual(len(client.feature_flags), 1)
|
|
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
|
|
|
|
@mock.patch("posthog.client.Poller")
|
|
@mock.patch("posthog.client.get")
|
|
def test_enable_local_evaluation_true_starts_poller(self, patch_get, patch_poller):
|
|
"""Test that when enable_local_evaluation=True (default), the poller is started"""
|
|
patch_get.return_value = {
|
|
"flags": [
|
|
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True}
|
|
],
|
|
"group_type_mapping": {},
|
|
"cohorts": {},
|
|
}
|
|
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
personal_api_key="test-personal-key",
|
|
enable_local_evaluation=True,
|
|
)
|
|
|
|
# Load feature flags should start the poller
|
|
client.load_feature_flags()
|
|
|
|
# Assert that the poller was created and started
|
|
patch_poller.assert_called_once()
|
|
patch_get.assert_called_once()
|
|
self.assertEqual(len(client.feature_flags), 1)
|
|
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
|
|
|
|
@mock.patch("posthog.client.remote_config")
|
|
def test_get_remote_config_payload_works_without_poller(self, patch_remote_config):
|
|
"""Test that get_remote_config_payload works without local evaluation enabled"""
|
|
patch_remote_config.return_value = {"test": "payload"}
|
|
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
personal_api_key="test-personal-key",
|
|
enable_local_evaluation=False,
|
|
)
|
|
|
|
# Should work without poller
|
|
result = client.get_remote_config_payload("test-flag")
|
|
|
|
self.assertEqual(result, {"test": "payload"})
|
|
patch_remote_config.assert_called_once_with(
|
|
"test-personal-key",
|
|
FAKE_TEST_API_KEY,
|
|
client.host,
|
|
"test-flag",
|
|
timeout=client.feature_flags_request_timeout_seconds,
|
|
)
|
|
|
|
def test_get_remote_config_payload_requires_personal_api_key(self):
|
|
"""Test that get_remote_config_payload requires personal API key"""
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
enable_local_evaluation=False,
|
|
)
|
|
|
|
result = client.get_remote_config_payload("test-flag")
|
|
|
|
self.assertIsNone(result)
|
|
|
|
def test_parse_send_feature_flags_method(self):
|
|
"""Test the _parse_send_feature_flags helper method"""
|
|
client = Client(FAKE_TEST_API_KEY, sync_mode=True)
|
|
|
|
# Test boolean True
|
|
result = client._parse_send_feature_flags(True)
|
|
expected = {
|
|
"should_send": True,
|
|
"only_evaluate_locally": None,
|
|
"person_properties": None,
|
|
"group_properties": None,
|
|
}
|
|
self.assertEqual(result, expected)
|
|
|
|
# Test boolean False
|
|
result = client._parse_send_feature_flags(False)
|
|
expected = {
|
|
"should_send": False,
|
|
"only_evaluate_locally": None,
|
|
"person_properties": None,
|
|
"group_properties": None,
|
|
}
|
|
self.assertEqual(result, expected)
|
|
|
|
# Test options dict with all fields
|
|
options = {
|
|
"only_evaluate_locally": True,
|
|
"person_properties": {"plan": "premium"},
|
|
"group_properties": {"company": {"type": "enterprise"}},
|
|
}
|
|
result = client._parse_send_feature_flags(options)
|
|
expected = {
|
|
"should_send": True,
|
|
"only_evaluate_locally": True,
|
|
"person_properties": {"plan": "premium"},
|
|
"group_properties": {"company": {"type": "enterprise"}},
|
|
}
|
|
self.assertEqual(result, expected)
|
|
|
|
# Test options dict with partial fields
|
|
options = {"person_properties": {"user_id": "123"}}
|
|
result = client._parse_send_feature_flags(options)
|
|
expected = {
|
|
"should_send": True,
|
|
"only_evaluate_locally": None,
|
|
"person_properties": {"user_id": "123"},
|
|
"group_properties": None,
|
|
}
|
|
self.assertEqual(result, expected)
|
|
|
|
# Test empty dict
|
|
result = client._parse_send_feature_flags({})
|
|
expected = {
|
|
"should_send": True,
|
|
"only_evaluate_locally": None,
|
|
"person_properties": None,
|
|
"group_properties": None,
|
|
}
|
|
self.assertEqual(result, expected)
|
|
|
|
# Test invalid types
|
|
with self.assertRaises(TypeError) as cm:
|
|
client._parse_send_feature_flags("invalid")
|
|
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
|
|
|
with self.assertRaises(TypeError) as cm:
|
|
client._parse_send_feature_flags(123)
|
|
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
|
|
|
with self.assertRaises(TypeError) as cm:
|
|
client._parse_send_feature_flags(None)
|
|
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
|
|
|
@mock.patch("posthog.client.batch_post")
|
|
def test_get_feature_flag_result_with_empty_string_payload(self, patch_batch_post):
|
|
"""Test that get_feature_flag_result returns a FeatureFlagResult when payload is empty string"""
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
personal_api_key="test_personal_api_key",
|
|
sync_mode=True,
|
|
)
|
|
|
|
# Set up local evaluation with a flag that has empty string payload
|
|
client.feature_flags = [
|
|
{
|
|
"id": 1,
|
|
"name": "Test flag",
|
|
"key": "test-flag",
|
|
"is_simple_flag": False,
|
|
"active": True,
|
|
"rollout_percentage": None,
|
|
"filters": {
|
|
"groups": [
|
|
{
|
|
"properties": [],
|
|
"rollout_percentage": None,
|
|
"variant": "empty-variant",
|
|
}
|
|
],
|
|
"multivariate": {
|
|
"variants": [
|
|
{
|
|
"key": "empty-variant",
|
|
"name": "Empty Variant",
|
|
"rollout_percentage": 100,
|
|
}
|
|
]
|
|
},
|
|
"payloads": {"empty-variant": ""}, # Empty string payload
|
|
},
|
|
}
|
|
]
|
|
|
|
# Test get_feature_flag_result
|
|
result = client.get_feature_flag_result(
|
|
"test-flag", "test-user", only_evaluate_locally=True
|
|
)
|
|
|
|
# Should return a FeatureFlagResult, not None
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result.key, "test-flag")
|
|
self.assertEqual(result.get_value(), "empty-variant")
|
|
self.assertEqual(result.payload, "") # Should be empty string, not None
|
|
|
|
@mock.patch("posthog.client.batch_post")
|
|
def test_get_all_flags_and_payloads_with_empty_string(self, patch_batch_post):
|
|
"""Test that get_all_flags_and_payloads includes flags with empty string payloads"""
|
|
client = Client(
|
|
FAKE_TEST_API_KEY,
|
|
personal_api_key="test_personal_api_key",
|
|
sync_mode=True,
|
|
)
|
|
|
|
# Set up multiple flags with different payload types
|
|
client.feature_flags = [
|
|
{
|
|
"id": 1,
|
|
"name": "Flag with empty payload",
|
|
"key": "empty-payload-flag",
|
|
"is_simple_flag": False,
|
|
"active": True,
|
|
"filters": {
|
|
"groups": [{"properties": [], "variant": "variant1"}],
|
|
"multivariate": {
|
|
"variants": [{"key": "variant1", "rollout_percentage": 100}]
|
|
},
|
|
"payloads": {"variant1": ""}, # Empty string
|
|
},
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Flag with normal payload",
|
|
"key": "normal-payload-flag",
|
|
"is_simple_flag": False,
|
|
"active": True,
|
|
"filters": {
|
|
"groups": [{"properties": [], "variant": "variant2"}],
|
|
"multivariate": {
|
|
"variants": [{"key": "variant2", "rollout_percentage": 100}]
|
|
},
|
|
"payloads": {"variant2": "normal payload"},
|
|
},
|
|
},
|
|
]
|
|
|
|
result = client.get_all_flags_and_payloads(
|
|
"test-user", only_evaluate_locally=True
|
|
)
|
|
|
|
# Check that both flags are included
|
|
self.assertEqual(result["featureFlags"]["empty-payload-flag"], "variant1")
|
|
self.assertEqual(result["featureFlags"]["normal-payload-flag"], "variant2")
|
|
|
|
# Check that empty string payload is included (not filtered out)
|
|
self.assertIn("empty-payload-flag", result["featureFlagPayloads"])
|
|
self.assertEqual(result["featureFlagPayloads"]["empty-payload-flag"], "")
|
|
self.assertEqual(
|
|
result["featureFlagPayloads"]["normal-payload-flag"], "normal payload"
|
|
)
|