posthoganalytics 6.2.1__tar.gz → 6.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {posthoganalytics-6.2.1/posthoganalytics.egg-info → posthoganalytics-6.3.0}/PKG-INFO +1 -1
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/langchain/callbacks.py +12 -13
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/args.py +6 -3
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/client.py +60 -4
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_client.py +254 -1
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/types.py +18 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/version.py +1 -1
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0/posthoganalytics.egg-info}/PKG-INFO +1 -1
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/LICENSE +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/MANIFEST.in +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/README.md +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/anthropic/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/anthropic/anthropic.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/anthropic/anthropic_async.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/anthropic/anthropic_providers.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/gemini/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/gemini/gemini.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/langchain/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/openai/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/openai/openai.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/openai/openai_async.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/openai/openai_providers.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/utils.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/consumer.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/contexts.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/exception_capture.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/exception_utils.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/feature_flags.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/integrations/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/integrations/django.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/poller.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/py.typed +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/request.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_before_send.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_consumer.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_contexts.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_exception_capture.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_feature_flag.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_feature_flag_result.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_feature_flags.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_module.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_request.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_size_limited_dict.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_types.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_utils.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/utils.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics.egg-info/SOURCES.txt +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics.egg-info/dependency_links.txt +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics.egg-info/requires.txt +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics.egg-info/top_level.txt +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/pyproject.toml +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/setup.cfg +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/setup.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/setup_analytics.py +0 -0
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/langchain/callbacks.py
RENAMED
|
@@ -33,7 +33,7 @@ from langchain_core.messages import (
|
|
|
33
33
|
from langchain_core.outputs import ChatGeneration, LLMResult
|
|
34
34
|
from pydantic import BaseModel
|
|
35
35
|
|
|
36
|
-
from posthoganalytics import
|
|
36
|
+
from posthoganalytics import setup
|
|
37
37
|
from posthoganalytics.ai.utils import get_model_params, with_privacy_mode
|
|
38
38
|
from posthoganalytics.client import Client
|
|
39
39
|
|
|
@@ -81,7 +81,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
81
81
|
The PostHog LLM observability callback handler for LangChain.
|
|
82
82
|
"""
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
_ph_client: Client
|
|
85
85
|
"""PostHog client instance."""
|
|
86
86
|
|
|
87
87
|
_distinct_id: Optional[Union[str, int, UUID]]
|
|
@@ -127,10 +127,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
127
127
|
privacy_mode: Whether to redact the input and output of the trace.
|
|
128
128
|
groups: Optional additional PostHog groups to use for the trace.
|
|
129
129
|
"""
|
|
130
|
-
|
|
131
|
-
if posthog_client is None:
|
|
132
|
-
raise ValueError("PostHog client is required")
|
|
133
|
-
self._client = posthog_client
|
|
130
|
+
self._ph_client = client or setup()
|
|
134
131
|
self._distinct_id = distinct_id
|
|
135
132
|
self._trace_id = trace_id
|
|
136
133
|
self._properties = properties or {}
|
|
@@ -481,7 +478,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
481
478
|
event_properties = {
|
|
482
479
|
"$ai_trace_id": trace_id,
|
|
483
480
|
"$ai_input_state": with_privacy_mode(
|
|
484
|
-
self.
|
|
481
|
+
self._ph_client, self._privacy_mode, run.input
|
|
485
482
|
),
|
|
486
483
|
"$ai_latency": run.latency,
|
|
487
484
|
"$ai_span_name": run.name,
|
|
@@ -497,13 +494,13 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
497
494
|
event_properties["$ai_is_error"] = True
|
|
498
495
|
elif outputs is not None:
|
|
499
496
|
event_properties["$ai_output_state"] = with_privacy_mode(
|
|
500
|
-
self.
|
|
497
|
+
self._ph_client, self._privacy_mode, outputs
|
|
501
498
|
)
|
|
502
499
|
|
|
503
500
|
if self._distinct_id is None:
|
|
504
501
|
event_properties["$process_person_profile"] = False
|
|
505
502
|
|
|
506
|
-
self.
|
|
503
|
+
self._ph_client.capture(
|
|
507
504
|
distinct_id=self._distinct_id or run_id,
|
|
508
505
|
event=event_name,
|
|
509
506
|
properties=event_properties,
|
|
@@ -550,14 +547,16 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
550
547
|
"$ai_provider": run.provider,
|
|
551
548
|
"$ai_model": run.model,
|
|
552
549
|
"$ai_model_parameters": run.model_params,
|
|
553
|
-
"$ai_input": with_privacy_mode(
|
|
550
|
+
"$ai_input": with_privacy_mode(
|
|
551
|
+
self._ph_client, self._privacy_mode, run.input
|
|
552
|
+
),
|
|
554
553
|
"$ai_http_status": 200,
|
|
555
554
|
"$ai_latency": run.latency,
|
|
556
555
|
"$ai_base_url": run.base_url,
|
|
557
556
|
}
|
|
558
557
|
if run.tools:
|
|
559
558
|
event_properties["$ai_tools"] = with_privacy_mode(
|
|
560
|
-
self.
|
|
559
|
+
self._ph_client,
|
|
561
560
|
self._privacy_mode,
|
|
562
561
|
run.tools,
|
|
563
562
|
)
|
|
@@ -589,7 +588,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
589
588
|
_extract_raw_esponse(generation) for generation in generation_result
|
|
590
589
|
]
|
|
591
590
|
event_properties["$ai_output_choices"] = with_privacy_mode(
|
|
592
|
-
self.
|
|
591
|
+
self._ph_client, self._privacy_mode, completions
|
|
593
592
|
)
|
|
594
593
|
|
|
595
594
|
if self._properties:
|
|
@@ -598,7 +597,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
598
597
|
if self._distinct_id is None:
|
|
599
598
|
event_properties["$process_person_profile"] = False
|
|
600
599
|
|
|
601
|
-
self.
|
|
600
|
+
self._ph_client.capture(
|
|
602
601
|
distinct_id=self._distinct_id or trace_id,
|
|
603
602
|
event="$ai_generation",
|
|
604
603
|
properties=event_properties,
|
|
@@ -5,6 +5,8 @@ from datetime import datetime
|
|
|
5
5
|
import numbers
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
|
+
from posthoganalytics.types import SendFeatureFlagsOptions
|
|
9
|
+
|
|
8
10
|
ID_TYPES = Union[numbers.Number, str, UUID, int]
|
|
9
11
|
|
|
10
12
|
|
|
@@ -22,7 +24,8 @@ class OptionalCaptureArgs(TypedDict):
|
|
|
22
24
|
error ID if you capture an exception).
|
|
23
25
|
groups: Group identifiers to associate with this event (format: {group_type: group_key})
|
|
24
26
|
send_feature_flags: Whether to include currently active feature flags in the event properties.
|
|
25
|
-
|
|
27
|
+
Can be a boolean (True/False) or a SendFeatureFlagsOptions object for advanced configuration.
|
|
28
|
+
Defaults to False.
|
|
26
29
|
disable_geoip: Whether to disable GeoIP lookup for this event. Defaults to False.
|
|
27
30
|
"""
|
|
28
31
|
|
|
@@ -32,8 +35,8 @@ class OptionalCaptureArgs(TypedDict):
|
|
|
32
35
|
uuid: NotRequired[Optional[str]]
|
|
33
36
|
groups: NotRequired[Optional[Dict[str, str]]]
|
|
34
37
|
send_feature_flags: NotRequired[
|
|
35
|
-
Optional[bool]
|
|
36
|
-
] #
|
|
38
|
+
Optional[Union[bool, SendFeatureFlagsOptions]]
|
|
39
|
+
] # Updated to support both boolean and options object
|
|
37
40
|
disable_geoip: NotRequired[
|
|
38
41
|
Optional[bool]
|
|
39
42
|
] # As above, optional so we can tell if the user is intentionally overriding a client setting or not
|
|
@@ -524,11 +524,31 @@ class Client(object):
|
|
|
524
524
|
|
|
525
525
|
extra_properties: dict[str, Any] = {}
|
|
526
526
|
feature_variants: Optional[dict[str, Union[bool, str]]] = {}
|
|
527
|
-
|
|
527
|
+
|
|
528
|
+
# Parse and normalize send_feature_flags parameter
|
|
529
|
+
flag_options = self._parse_send_feature_flags(send_feature_flags)
|
|
530
|
+
|
|
531
|
+
if flag_options["should_send"]:
|
|
528
532
|
try:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
533
|
+
if flag_options["only_evaluate_locally"] is True:
|
|
534
|
+
# Only use local evaluation
|
|
535
|
+
feature_variants = self.get_all_flags(
|
|
536
|
+
distinct_id,
|
|
537
|
+
groups=(groups or {}),
|
|
538
|
+
person_properties=flag_options["person_properties"],
|
|
539
|
+
group_properties=flag_options["group_properties"],
|
|
540
|
+
disable_geoip=disable_geoip,
|
|
541
|
+
only_evaluate_locally=True,
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
# Default behavior - use remote evaluation
|
|
545
|
+
feature_variants = self.get_feature_variants(
|
|
546
|
+
distinct_id,
|
|
547
|
+
groups,
|
|
548
|
+
person_properties=flag_options["person_properties"],
|
|
549
|
+
group_properties=flag_options["group_properties"],
|
|
550
|
+
disable_geoip=disable_geoip,
|
|
551
|
+
)
|
|
532
552
|
except Exception as e:
|
|
533
553
|
self.log.exception(
|
|
534
554
|
f"[FEATURE FLAGS] Unable to get feature variants: {e}"
|
|
@@ -559,6 +579,42 @@ class Client(object):
|
|
|
559
579
|
|
|
560
580
|
return self._enqueue(msg, disable_geoip)
|
|
561
581
|
|
|
582
|
+
def _parse_send_feature_flags(self, send_feature_flags) -> dict:
|
|
583
|
+
"""
|
|
584
|
+
Parse and normalize send_feature_flags parameter into a standard format.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
send_feature_flags: Either bool or SendFeatureFlagsOptions dict
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
dict: Normalized options with keys: should_send, only_evaluate_locally,
|
|
591
|
+
person_properties, group_properties
|
|
592
|
+
|
|
593
|
+
Raises:
|
|
594
|
+
TypeError: If send_feature_flags is not bool or dict
|
|
595
|
+
"""
|
|
596
|
+
if isinstance(send_feature_flags, dict):
|
|
597
|
+
return {
|
|
598
|
+
"should_send": True,
|
|
599
|
+
"only_evaluate_locally": send_feature_flags.get(
|
|
600
|
+
"only_evaluate_locally"
|
|
601
|
+
),
|
|
602
|
+
"person_properties": send_feature_flags.get("person_properties"),
|
|
603
|
+
"group_properties": send_feature_flags.get("group_properties"),
|
|
604
|
+
}
|
|
605
|
+
elif isinstance(send_feature_flags, bool):
|
|
606
|
+
return {
|
|
607
|
+
"should_send": send_feature_flags,
|
|
608
|
+
"only_evaluate_locally": None,
|
|
609
|
+
"person_properties": None,
|
|
610
|
+
"group_properties": None,
|
|
611
|
+
}
|
|
612
|
+
else:
|
|
613
|
+
raise TypeError(
|
|
614
|
+
f"Invalid type for send_feature_flags: {type(send_feature_flags)}. "
|
|
615
|
+
f"Expected bool or dict."
|
|
616
|
+
)
|
|
617
|
+
|
|
562
618
|
def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
|
|
563
619
|
"""
|
|
564
620
|
Set properties on a person profile.
|
|
@@ -751,6 +751,186 @@ class TestClient(unittest.TestCase):
|
|
|
751
751
|
|
|
752
752
|
self.assertEqual(patch_flags.call_count, 0)
|
|
753
753
|
|
|
754
|
+
@mock.patch("posthog.client.flags")
|
|
755
|
+
def test_capture_with_send_feature_flags_options_only_evaluate_locally_true(
|
|
756
|
+
self, patch_flags
|
|
757
|
+
):
|
|
758
|
+
"""Test that SendFeatureFlagsOptions with only_evaluate_locally=True uses local evaluation"""
|
|
759
|
+
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
760
|
+
client = Client(
|
|
761
|
+
FAKE_TEST_API_KEY,
|
|
762
|
+
on_error=self.set_fail,
|
|
763
|
+
personal_api_key=FAKE_TEST_API_KEY,
|
|
764
|
+
sync_mode=True,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# Set up local flags
|
|
768
|
+
client.feature_flags = [
|
|
769
|
+
{
|
|
770
|
+
"id": 1,
|
|
771
|
+
"key": "local-flag",
|
|
772
|
+
"active": True,
|
|
773
|
+
"filters": {
|
|
774
|
+
"groups": [
|
|
775
|
+
{
|
|
776
|
+
"properties": [{"key": "region", "value": "US"}],
|
|
777
|
+
"rollout_percentage": 100,
|
|
778
|
+
}
|
|
779
|
+
],
|
|
780
|
+
},
|
|
781
|
+
}
|
|
782
|
+
]
|
|
783
|
+
|
|
784
|
+
send_options = {
|
|
785
|
+
"only_evaluate_locally": True,
|
|
786
|
+
"person_properties": {"region": "US"},
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
msg_uuid = client.capture(
|
|
790
|
+
"test event", distinct_id="distinct_id", send_feature_flags=send_options
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
self.assertIsNotNone(msg_uuid)
|
|
794
|
+
self.assertFalse(self.failed)
|
|
795
|
+
|
|
796
|
+
# Verify flags() was not called (no remote evaluation)
|
|
797
|
+
patch_flags.assert_not_called()
|
|
798
|
+
|
|
799
|
+
# Check the message includes the local flag
|
|
800
|
+
mock_post.assert_called_once()
|
|
801
|
+
batch_data = mock_post.call_args[1]["batch"]
|
|
802
|
+
msg = batch_data[0]
|
|
803
|
+
|
|
804
|
+
self.assertEqual(msg["properties"]["$feature/local-flag"], True)
|
|
805
|
+
self.assertEqual(msg["properties"]["$active_feature_flags"], ["local-flag"])
|
|
806
|
+
|
|
807
|
+
@mock.patch("posthog.client.flags")
|
|
808
|
+
def test_capture_with_send_feature_flags_options_only_evaluate_locally_false(
|
|
809
|
+
self, patch_flags
|
|
810
|
+
):
|
|
811
|
+
"""Test that SendFeatureFlagsOptions with only_evaluate_locally=False forces remote evaluation"""
|
|
812
|
+
patch_flags.return_value = {"featureFlags": {"remote-flag": "remote-value"}}
|
|
813
|
+
|
|
814
|
+
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
815
|
+
client = Client(
|
|
816
|
+
FAKE_TEST_API_KEY,
|
|
817
|
+
on_error=self.set_fail,
|
|
818
|
+
personal_api_key=FAKE_TEST_API_KEY,
|
|
819
|
+
sync_mode=True,
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
send_options = {
|
|
823
|
+
"only_evaluate_locally": False,
|
|
824
|
+
"person_properties": {"plan": "premium"},
|
|
825
|
+
"group_properties": {"company": {"type": "enterprise"}},
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
msg_uuid = client.capture(
|
|
829
|
+
"test event",
|
|
830
|
+
distinct_id="distinct_id",
|
|
831
|
+
groups={"company": "acme"},
|
|
832
|
+
send_feature_flags=send_options,
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
self.assertIsNotNone(msg_uuid)
|
|
836
|
+
self.assertFalse(self.failed)
|
|
837
|
+
|
|
838
|
+
# Verify flags() was called with the correct properties
|
|
839
|
+
patch_flags.assert_called_once()
|
|
840
|
+
call_args = patch_flags.call_args[1]
|
|
841
|
+
self.assertEqual(call_args["person_properties"], {"plan": "premium"})
|
|
842
|
+
self.assertEqual(
|
|
843
|
+
call_args["group_properties"], {"company": {"type": "enterprise"}}
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
# Check the message includes the remote flag
|
|
847
|
+
mock_post.assert_called_once()
|
|
848
|
+
batch_data = mock_post.call_args[1]["batch"]
|
|
849
|
+
msg = batch_data[0]
|
|
850
|
+
|
|
851
|
+
self.assertEqual(msg["properties"]["$feature/remote-flag"], "remote-value")
|
|
852
|
+
|
|
853
|
+
@mock.patch("posthog.client.flags")
|
|
854
|
+
def test_capture_with_send_feature_flags_options_default_behavior(
|
|
855
|
+
self, patch_flags
|
|
856
|
+
):
|
|
857
|
+
"""Test that SendFeatureFlagsOptions without only_evaluate_locally defaults to remote evaluation"""
|
|
858
|
+
patch_flags.return_value = {"featureFlags": {"default-flag": "default-value"}}
|
|
859
|
+
|
|
860
|
+
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
861
|
+
client = Client(
|
|
862
|
+
FAKE_TEST_API_KEY,
|
|
863
|
+
on_error=self.set_fail,
|
|
864
|
+
personal_api_key=FAKE_TEST_API_KEY,
|
|
865
|
+
sync_mode=True,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
send_options = {
|
|
869
|
+
"person_properties": {"subscription": "pro"},
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
msg_uuid = client.capture(
|
|
873
|
+
"test event", distinct_id="distinct_id", send_feature_flags=send_options
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
self.assertIsNotNone(msg_uuid)
|
|
877
|
+
self.assertFalse(self.failed)
|
|
878
|
+
|
|
879
|
+
# Verify flags() was called (default to remote evaluation)
|
|
880
|
+
patch_flags.assert_called_once()
|
|
881
|
+
call_args = patch_flags.call_args[1]
|
|
882
|
+
self.assertEqual(call_args["person_properties"], {"subscription": "pro"})
|
|
883
|
+
|
|
884
|
+
# Check the message includes the flag
|
|
885
|
+
mock_post.assert_called_once()
|
|
886
|
+
batch_data = mock_post.call_args[1]["batch"]
|
|
887
|
+
msg = batch_data[0]
|
|
888
|
+
|
|
889
|
+
self.assertEqual(
|
|
890
|
+
msg["properties"]["$feature/default-flag"], "default-value"
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
@mock.patch("posthog.client.flags")
|
|
894
|
+
def test_capture_exception_with_send_feature_flags_options(self, patch_flags):
|
|
895
|
+
"""Test that capture_exception also supports SendFeatureFlagsOptions"""
|
|
896
|
+
patch_flags.return_value = {"featureFlags": {"exception-flag": True}}
|
|
897
|
+
|
|
898
|
+
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
899
|
+
client = Client(
|
|
900
|
+
FAKE_TEST_API_KEY,
|
|
901
|
+
on_error=self.set_fail,
|
|
902
|
+
personal_api_key=FAKE_TEST_API_KEY,
|
|
903
|
+
sync_mode=True,
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
send_options = {
|
|
907
|
+
"only_evaluate_locally": False,
|
|
908
|
+
"person_properties": {"user_type": "admin"},
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
try:
|
|
912
|
+
raise ValueError("Test exception")
|
|
913
|
+
except ValueError as e:
|
|
914
|
+
msg_uuid = client.capture_exception(
|
|
915
|
+
e, distinct_id="distinct_id", send_feature_flags=send_options
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
self.assertIsNotNone(msg_uuid)
|
|
919
|
+
self.assertFalse(self.failed)
|
|
920
|
+
|
|
921
|
+
# Verify flags() was called with the correct properties
|
|
922
|
+
patch_flags.assert_called_once()
|
|
923
|
+
call_args = patch_flags.call_args[1]
|
|
924
|
+
self.assertEqual(call_args["person_properties"], {"user_type": "admin"})
|
|
925
|
+
|
|
926
|
+
# Check the message includes the flag
|
|
927
|
+
mock_post.assert_called_once()
|
|
928
|
+
batch_data = mock_post.call_args[1]["batch"]
|
|
929
|
+
msg = batch_data[0]
|
|
930
|
+
|
|
931
|
+
self.assertEqual(msg["event"], "$exception")
|
|
932
|
+
self.assertEqual(msg["properties"]["$feature/exception-flag"], True)
|
|
933
|
+
|
|
754
934
|
def test_stringifies_distinct_id(self):
|
|
755
935
|
# A large number that loses precision in node:
|
|
756
936
|
# node -e "console.log(157963456373623802 + 1)" > 157963456373623800
|
|
@@ -1591,7 +1771,7 @@ class TestClient(unittest.TestCase):
|
|
|
1591
1771
|
|
|
1592
1772
|
@mock.patch("posthog.client.Poller")
|
|
1593
1773
|
@mock.patch("posthog.client.get")
|
|
1594
|
-
def test_call_identify_fails(self, patch_get,
|
|
1774
|
+
def test_call_identify_fails(self, patch_get, patch_poller):
|
|
1595
1775
|
def raise_effect():
|
|
1596
1776
|
raise Exception("http exception")
|
|
1597
1777
|
|
|
@@ -1993,3 +2173,76 @@ class TestClient(unittest.TestCase):
|
|
|
1993
2173
|
result = client.get_remote_config_payload("test-flag")
|
|
1994
2174
|
|
|
1995
2175
|
self.assertIsNone(result)
|
|
2176
|
+
|
|
2177
|
+
def test_parse_send_feature_flags_method(self):
|
|
2178
|
+
"""Test the _parse_send_feature_flags helper method"""
|
|
2179
|
+
client = Client(FAKE_TEST_API_KEY, sync_mode=True)
|
|
2180
|
+
|
|
2181
|
+
# Test boolean True
|
|
2182
|
+
result = client._parse_send_feature_flags(True)
|
|
2183
|
+
expected = {
|
|
2184
|
+
"should_send": True,
|
|
2185
|
+
"only_evaluate_locally": None,
|
|
2186
|
+
"person_properties": None,
|
|
2187
|
+
"group_properties": None,
|
|
2188
|
+
}
|
|
2189
|
+
self.assertEqual(result, expected)
|
|
2190
|
+
|
|
2191
|
+
# Test boolean False
|
|
2192
|
+
result = client._parse_send_feature_flags(False)
|
|
2193
|
+
expected = {
|
|
2194
|
+
"should_send": False,
|
|
2195
|
+
"only_evaluate_locally": None,
|
|
2196
|
+
"person_properties": None,
|
|
2197
|
+
"group_properties": None,
|
|
2198
|
+
}
|
|
2199
|
+
self.assertEqual(result, expected)
|
|
2200
|
+
|
|
2201
|
+
# Test options dict with all fields
|
|
2202
|
+
options = {
|
|
2203
|
+
"only_evaluate_locally": True,
|
|
2204
|
+
"person_properties": {"plan": "premium"},
|
|
2205
|
+
"group_properties": {"company": {"type": "enterprise"}},
|
|
2206
|
+
}
|
|
2207
|
+
result = client._parse_send_feature_flags(options)
|
|
2208
|
+
expected = {
|
|
2209
|
+
"should_send": True,
|
|
2210
|
+
"only_evaluate_locally": True,
|
|
2211
|
+
"person_properties": {"plan": "premium"},
|
|
2212
|
+
"group_properties": {"company": {"type": "enterprise"}},
|
|
2213
|
+
}
|
|
2214
|
+
self.assertEqual(result, expected)
|
|
2215
|
+
|
|
2216
|
+
# Test options dict with partial fields
|
|
2217
|
+
options = {"person_properties": {"user_id": "123"}}
|
|
2218
|
+
result = client._parse_send_feature_flags(options)
|
|
2219
|
+
expected = {
|
|
2220
|
+
"should_send": True,
|
|
2221
|
+
"only_evaluate_locally": None,
|
|
2222
|
+
"person_properties": {"user_id": "123"},
|
|
2223
|
+
"group_properties": None,
|
|
2224
|
+
}
|
|
2225
|
+
self.assertEqual(result, expected)
|
|
2226
|
+
|
|
2227
|
+
# Test empty dict
|
|
2228
|
+
result = client._parse_send_feature_flags({})
|
|
2229
|
+
expected = {
|
|
2230
|
+
"should_send": True,
|
|
2231
|
+
"only_evaluate_locally": None,
|
|
2232
|
+
"person_properties": None,
|
|
2233
|
+
"group_properties": None,
|
|
2234
|
+
}
|
|
2235
|
+
self.assertEqual(result, expected)
|
|
2236
|
+
|
|
2237
|
+
# Test invalid types
|
|
2238
|
+
with self.assertRaises(TypeError) as cm:
|
|
2239
|
+
client._parse_send_feature_flags("invalid")
|
|
2240
|
+
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
|
2241
|
+
|
|
2242
|
+
with self.assertRaises(TypeError) as cm:
|
|
2243
|
+
client._parse_send_feature_flags(123)
|
|
2244
|
+
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
|
2245
|
+
|
|
2246
|
+
with self.assertRaises(TypeError) as cm:
|
|
2247
|
+
client._parse_send_feature_flags(None)
|
|
2248
|
+
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
|
@@ -9,6 +9,24 @@ FlagValue = Union[bool, str]
|
|
|
9
9
|
BeforeSendCallback = Callable[[dict[str, Any]], Optional[dict[str, Any]]]
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
class SendFeatureFlagsOptions(TypedDict, total=False):
|
|
13
|
+
"""Options for sending feature flags with capture events.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
only_evaluate_locally: Whether to only use local evaluation for feature flags.
|
|
17
|
+
If True, only flags that can be evaluated locally will be included.
|
|
18
|
+
If False, remote evaluation via /flags API will be used when needed.
|
|
19
|
+
person_properties: Properties to use for feature flag evaluation specific to this event.
|
|
20
|
+
These properties will be merged with any existing person properties.
|
|
21
|
+
group_properties: Group properties to use for feature flag evaluation specific to this event.
|
|
22
|
+
Format: { group_type_name: { group_properties } }
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
only_evaluate_locally: Optional[bool]
|
|
26
|
+
person_properties: Optional[dict[str, Any]]
|
|
27
|
+
group_properties: Optional[dict[str, dict[str, Any]]]
|
|
28
|
+
|
|
29
|
+
|
|
12
30
|
@dataclass(frozen=True)
|
|
13
31
|
class FlagReason:
|
|
14
32
|
code: str
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/anthropic/anthropic.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/anthropic/anthropic_async.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/openai/openai_async.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/ai/openai/openai_providers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_exception_capture.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_feature_flag.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_feature_flag_result.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_feature_flags.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics/test/test_size_limited_dict.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.0}/posthoganalytics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|