posthoganalytics 6.1.1__py3-none-any.whl → 6.3.0__py3-none-any.whl

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.
@@ -676,7 +676,7 @@ def shutdown():
676
676
  _proxy("join")
677
677
 
678
678
 
679
- def setup():
679
+ def setup() -> Client:
680
680
  global default_client
681
681
  if not default_client:
682
682
  if not api_key:
@@ -706,6 +706,8 @@ def setup():
706
706
  default_client.disabled = disabled
707
707
  default_client.debug = debug
708
708
 
709
+ return default_client
710
+
709
711
 
710
712
  def _proxy(method, *args, **kwargs):
711
713
  """Create an analytics client if one doesn't exist and send to it."""
@@ -8,7 +8,7 @@ except ImportError:
8
8
 
9
9
  import time
10
10
  import uuid
11
- from typing import Any, Dict, Optional
11
+ from typing import Any, Dict, Optional, cast
12
12
 
13
13
  from posthoganalytics.ai.utils import (
14
14
  call_llm_and_track_usage,
@@ -17,6 +17,7 @@ from posthoganalytics.ai.utils import (
17
17
  with_privacy_mode,
18
18
  )
19
19
  from posthoganalytics.client import Client as PostHogClient
20
+ from posthoganalytics import setup
20
21
 
21
22
 
22
23
  class Anthropic(anthropic.Anthropic):
@@ -26,14 +27,14 @@ class Anthropic(anthropic.Anthropic):
26
27
 
27
28
  _ph_client: PostHogClient
28
29
 
29
- def __init__(self, posthog_client: PostHogClient, **kwargs):
30
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
30
31
  """
31
32
  Args:
32
33
  posthog_client: PostHog client for tracking usage
33
34
  **kwargs: Additional arguments passed to the Anthropic client
34
35
  """
35
36
  super().__init__(**kwargs)
36
- self._ph_client = posthog_client
37
+ self._ph_client = posthog_client or setup()
37
38
  self.messages = WrappedMessages(self)
38
39
 
39
40
 
@@ -10,6 +10,7 @@ import time
10
10
  import uuid
11
11
  from typing import Any, Dict, Optional
12
12
 
13
+ from posthoganalytics import setup
13
14
  from posthoganalytics.ai.utils import (
14
15
  call_llm_and_track_usage_async,
15
16
  get_model_params,
@@ -26,14 +27,14 @@ class AsyncAnthropic(anthropic.AsyncAnthropic):
26
27
 
27
28
  _ph_client: PostHogClient
28
29
 
29
- def __init__(self, posthog_client: PostHogClient, **kwargs):
30
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
30
31
  """
31
32
  Args:
32
33
  posthog_client: PostHog client for tracking usage
33
34
  **kwargs: Additional arguments passed to the Anthropic client
34
35
  """
35
36
  super().__init__(**kwargs)
36
- self._ph_client = posthog_client
37
+ self._ph_client = posthog_client or setup()
37
38
  self.messages = AsyncWrappedMessages(self)
38
39
 
39
40
 
@@ -5,9 +5,12 @@ except ImportError:
5
5
  "Please install the Anthropic SDK to use this feature: 'pip install anthropic'"
6
6
  )
7
7
 
8
+ from typing import Optional
9
+
8
10
  from posthoganalytics.ai.anthropic.anthropic import WrappedMessages
9
11
  from posthoganalytics.ai.anthropic.anthropic_async import AsyncWrappedMessages
10
12
  from posthoganalytics.client import Client as PostHogClient
13
+ from posthoganalytics import setup
11
14
 
12
15
 
13
16
  class AnthropicBedrock(anthropic.AnthropicBedrock):
@@ -17,9 +20,9 @@ class AnthropicBedrock(anthropic.AnthropicBedrock):
17
20
 
18
21
  _ph_client: PostHogClient
19
22
 
20
- def __init__(self, posthog_client: PostHogClient, **kwargs):
23
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
21
24
  super().__init__(**kwargs)
22
- self._ph_client = posthog_client
25
+ self._ph_client = posthog_client or setup()
23
26
  self.messages = WrappedMessages(self)
24
27
 
25
28
 
@@ -30,9 +33,9 @@ class AsyncAnthropicBedrock(anthropic.AsyncAnthropicBedrock):
30
33
 
31
34
  _ph_client: PostHogClient
32
35
 
33
- def __init__(self, posthog_client: PostHogClient, **kwargs):
36
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
34
37
  super().__init__(**kwargs)
35
- self._ph_client = posthog_client
38
+ self._ph_client = posthog_client or setup()
36
39
  self.messages = AsyncWrappedMessages(self)
37
40
 
38
41
 
@@ -43,9 +46,9 @@ class AnthropicVertex(anthropic.AnthropicVertex):
43
46
 
44
47
  _ph_client: PostHogClient
45
48
 
46
- def __init__(self, posthog_client: PostHogClient, **kwargs):
49
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
47
50
  super().__init__(**kwargs)
48
- self._ph_client = posthog_client
51
+ self._ph_client = posthog_client or setup()
49
52
  self.messages = WrappedMessages(self)
50
53
 
51
54
 
@@ -56,7 +59,7 @@ class AsyncAnthropicVertex(anthropic.AsyncAnthropicVertex):
56
59
 
57
60
  _ph_client: PostHogClient
58
61
 
59
- def __init__(self, posthog_client: PostHogClient, **kwargs):
62
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
60
63
  super().__init__(**kwargs)
61
- self._ph_client = posthog_client
64
+ self._ph_client = posthog_client or setup()
62
65
  self.messages = AsyncWrappedMessages(self)
@@ -10,6 +10,7 @@ except ImportError:
10
10
  "Please install the Google Gemini SDK to use this feature: 'pip install google-genai'"
11
11
  )
12
12
 
13
+ from posthoganalytics import setup
13
14
  from posthoganalytics.ai.utils import (
14
15
  call_llm_and_track_usage,
15
16
  get_model_params,
@@ -36,6 +37,8 @@ class Client:
36
37
  )
37
38
  """
38
39
 
40
+ _ph_client: PostHogClient
41
+
39
42
  def __init__(
40
43
  self,
41
44
  api_key: Optional[str] = None,
@@ -56,12 +59,14 @@ class Client:
56
59
  posthog_groups: Default groups for all calls (can be overridden per call)
57
60
  **kwargs: Additional arguments (for future compatibility)
58
61
  """
59
- if posthog_client is None:
62
+ self._ph_client = posthog_client or setup()
63
+
64
+ if self._ph_client is None:
60
65
  raise ValueError("posthog_client is required for PostHog tracking")
61
66
 
62
67
  self.models = Models(
63
68
  api_key=api_key,
64
- posthog_client=posthog_client,
69
+ posthog_client=self._ph_client,
65
70
  posthog_distinct_id=posthog_distinct_id,
66
71
  posthog_properties=posthog_properties,
67
72
  posthog_privacy_mode=posthog_privacy_mode,
@@ -97,10 +102,10 @@ class Models:
97
102
  posthog_groups: Default groups for all calls
98
103
  **kwargs: Additional arguments (for future compatibility)
99
104
  """
100
- if posthog_client is None:
101
- raise ValueError("posthog_client is required for PostHog tracking")
105
+ self._ph_client = posthog_client or setup()
102
106
 
103
- self._ph_client = posthog_client
107
+ if self._ph_client is None:
108
+ raise ValueError("posthog_client is required for PostHog tracking")
104
109
 
105
110
  # Store default PostHog settings
106
111
  self._default_distinct_id = posthog_distinct_id
@@ -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 default_client
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
- _client: Client
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
- posthog_client = client or default_client
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._client, self._privacy_mode, run.input
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._client, self._privacy_mode, outputs
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._client.capture(
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(self._client, self._privacy_mode, run.input),
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._client,
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._client, self._privacy_mode, completions
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._client.capture(
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,
@@ -15,6 +15,7 @@ from posthoganalytics.ai.utils import (
15
15
  with_privacy_mode,
16
16
  )
17
17
  from posthoganalytics.client import Client as PostHogClient
18
+ from posthoganalytics import setup
18
19
 
19
20
 
20
21
  class OpenAI(openai.OpenAI):
@@ -24,16 +25,15 @@ class OpenAI(openai.OpenAI):
24
25
 
25
26
  _ph_client: PostHogClient
26
27
 
27
- def __init__(self, posthog_client: PostHogClient, **kwargs):
28
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
28
29
  """
29
30
  Args:
30
31
  api_key: OpenAI API key.
31
- posthog_client: If provided, events will be captured via this client instead
32
- of the global posthog.
32
+ posthog_client: If provided, events will be captured via this client instead of the global `posthog`.
33
33
  **openai_config: Any additional keyword args to set on openai (e.g. organization="xxx").
34
34
  """
35
35
  super().__init__(**kwargs)
36
- self._ph_client = posthog_client
36
+ self._ph_client = posthog_client or setup()
37
37
 
38
38
  # Store original objects after parent initialization (only if they exist)
39
39
  self._original_chat = getattr(self, "chat", None)
@@ -1,6 +1,6 @@
1
1
  import time
2
2
  import uuid
3
- from typing import Any, Dict, List, Optional
3
+ from typing import Any, Dict, List, Optional, cast
4
4
 
5
5
  try:
6
6
  import openai
@@ -9,6 +9,7 @@ except ImportError:
9
9
  "Please install the OpenAI SDK to use this feature: 'pip install openai'"
10
10
  )
11
11
 
12
+ from posthoganalytics import setup
12
13
  from posthoganalytics.ai.utils import (
13
14
  call_llm_and_track_usage_async,
14
15
  get_model_params,
@@ -24,7 +25,7 @@ class AsyncOpenAI(openai.AsyncOpenAI):
24
25
 
25
26
  _ph_client: PostHogClient
26
27
 
27
- def __init__(self, posthog_client: PostHogClient, **kwargs):
28
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
28
29
  """
29
30
  Args:
30
31
  api_key: OpenAI API key.
@@ -33,7 +34,7 @@ class AsyncOpenAI(openai.AsyncOpenAI):
33
34
  **openai_config: Any additional keyword args to set on openai (e.g. organization="xxx").
34
35
  """
35
36
  super().__init__(**kwargs)
36
- self._ph_client = posthog_client
37
+ self._ph_client = posthog_client or setup()
37
38
 
38
39
  # Store original objects after parent initialization (only if they exist)
39
40
  self._original_chat = getattr(self, "chat", None)
@@ -15,7 +15,10 @@ from posthoganalytics.ai.openai.openai_async import WrappedBeta as AsyncWrappedB
15
15
  from posthoganalytics.ai.openai.openai_async import WrappedChat as AsyncWrappedChat
16
16
  from posthoganalytics.ai.openai.openai_async import WrappedEmbeddings as AsyncWrappedEmbeddings
17
17
  from posthoganalytics.ai.openai.openai_async import WrappedResponses as AsyncWrappedResponses
18
+ from typing import Optional
19
+
18
20
  from posthoganalytics.client import Client as PostHogClient
21
+ from posthoganalytics import setup
19
22
 
20
23
 
21
24
  class AzureOpenAI(openai.AzureOpenAI):
@@ -25,7 +28,7 @@ class AzureOpenAI(openai.AzureOpenAI):
25
28
 
26
29
  _ph_client: PostHogClient
27
30
 
28
- def __init__(self, posthog_client: PostHogClient, **kwargs):
31
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
29
32
  """
30
33
  Args:
31
34
  api_key: Azure OpenAI API key.
@@ -34,7 +37,7 @@ class AzureOpenAI(openai.AzureOpenAI):
34
37
  **openai_config: Any additional keyword args to set on Azure OpenAI (e.g. azure_endpoint="xxx").
35
38
  """
36
39
  super().__init__(**kwargs)
37
- self._ph_client = posthog_client
40
+ self._ph_client = posthog_client or setup()
38
41
 
39
42
  # Store original objects after parent initialization (only if they exist)
40
43
  self._original_chat = getattr(self, "chat", None)
@@ -63,7 +66,7 @@ class AsyncAzureOpenAI(openai.AsyncAzureOpenAI):
63
66
 
64
67
  _ph_client: PostHogClient
65
68
 
66
- def __init__(self, posthog_client: PostHogClient, **kwargs):
69
+ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
67
70
  """
68
71
  Args:
69
72
  api_key: Azure OpenAI API key.
@@ -72,7 +75,7 @@ class AsyncAzureOpenAI(openai.AsyncAzureOpenAI):
72
75
  **openai_config: Any additional keyword args to set on Azure OpenAI (e.g. azure_endpoint="xxx").
73
76
  """
74
77
  super().__init__(**kwargs)
75
- self._ph_client = posthog_client
78
+ self._ph_client = posthog_client or setup()
76
79
 
77
80
  # Store original objects after parent initialization (only if they exist)
78
81
  self._original_chat = getattr(self, "chat", None)
posthoganalytics/args.py CHANGED
@@ -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
- Defaults to False
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
- ] # Optional so we can tell if the user is intentionally overriding a client setting or not
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
- if send_feature_flags:
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
- feature_variants = self.get_feature_variants(
530
- distinct_id, groups, disable_geoip=disable_geoip
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, patch_poll):
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))
posthoganalytics/types.py CHANGED
@@ -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
@@ -1,4 +1,4 @@
1
- VERSION = "6.1.1"
1
+ VERSION = "6.3.0"
2
2
 
3
3
  if __name__ == "__main__":
4
4
  print(VERSION, end="") # noqa: T201
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: posthoganalytics
3
- Version: 6.1.1
3
+ Version: 6.3.0
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -1,6 +1,6 @@
1
- posthoganalytics/__init__.py,sha256=TSi-Hq6mKCAiq6VAjaHPG5g8Pb8FTkK8De_l69O7G74,24096
2
- posthoganalytics/args.py,sha256=hRKPQ3cPGyDn4S7Ay9t2NlgoZg1cJ0GeN_Mb6OKtmfo,3145
3
- posthoganalytics/client.py,sha256=L_Ive-qlIvflqjLrote0TP2r9jBqbNsQE_Xs7gDEe4Y,65265
1
+ posthoganalytics/__init__.py,sha256=N_Mq_RDLFbd02HCMWxJPTtcSYj3NWcartohQKa7MNlw,24133
2
+ posthoganalytics/args.py,sha256=iZ2JWeANiAREJKhS-Qls9tIngjJOSfAVR8C4xFT5sHw,3307
3
+ posthoganalytics/client.py,sha256=y7bUrKYaCGPWnAkOfYrLEVFD7Buoc4dZor6yKB51O_E,67611
4
4
  posthoganalytics/consumer.py,sha256=CiNbJBdyW9jER3ZYCKbX-JFmEDXlE1lbDy1MSl43-a0,4617
5
5
  posthoganalytics/contexts.py,sha256=LFSFIYpUFWKTBnGMjV9n1aYHWbAzz5zLJGr2qG34PoE,9405
6
6
  posthoganalytics/exception_capture.py,sha256=1VHBfffrXXrkK0PT8iVgKPpj_R1pGAzG5f3Qw0WF79w,1783
@@ -9,28 +9,28 @@ posthoganalytics/feature_flags.py,sha256=O_kXmw3goB2E9XMBosdPeBAuo9MsnsH8PyNWq95
9
9
  posthoganalytics/poller.py,sha256=jBz5rfH_kn_bBz7wCB46Fpvso4ttx4uzqIZWvXBCFmQ,595
10
10
  posthoganalytics/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  posthoganalytics/request.py,sha256=TaeySYpcvHMf5Ftf5KqqlO0VPJpirKBCRrThlS04Kew,6124
12
- posthoganalytics/types.py,sha256=INxWBOEQc0xgPcap6FdQNSU7zuQBmKShYaGzyuHKql8,9128
12
+ posthoganalytics/types.py,sha256=2rwhiZd9lvs37MiXEBADVdMKvcCvFXfAMgIUJ8KNTBs,10005
13
13
  posthoganalytics/utils.py,sha256=-0w-OLcCaoldkbBebPzQyBzLJSo9G9yBOg8NDVz7La8,16088
14
- posthoganalytics/version.py,sha256=xZhcgW3hzlFW0J_tV0DpBWXhz8E1uxHP1x46lky-cjo,87
14
+ posthoganalytics/version.py,sha256=kh3XbIm02A3duSxLSxM9UICZfLJm0YJYGYgmKSdH210,87
15
15
  posthoganalytics/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  posthoganalytics/ai/utils.py,sha256=5-2XfmetCs0v9otBoux7-IEG933wAnKLSGS6oYLqCkw,19529
17
17
  posthoganalytics/ai/anthropic/__init__.py,sha256=fFhDOiRzTXzGQlgnrRDL-4yKC8EYIl8NW4a2QNR6xRU,368
18
- posthoganalytics/ai/anthropic/anthropic.py,sha256=5YekQFUio_SRMgdI2y7gbEKFrp-dk1Vi333662doRxc,7328
19
- posthoganalytics/ai/anthropic/anthropic_async.py,sha256=4ncBLLhhPUQ46XzHz6YRlBt-jBBdP6ol-6V_78AmdAY,7448
20
- posthoganalytics/ai/anthropic/anthropic_providers.py,sha256=6gnL_Z43FTar2TGNPDudPXEZawpQfVx7zO4cLndz9r4,1963
18
+ posthoganalytics/ai/anthropic/anthropic.py,sha256=P8o-pZ2rbJXDiHO73OWjO7OgboGiEm_wVY4pbvHnUEs,7397
19
+ posthoganalytics/ai/anthropic/anthropic_async.py,sha256=iAwVlAY6VeW0dGZdMkdfniBTBFUdZZrDMZi-O9vdiuo,7511
20
+ posthoganalytics/ai/anthropic/anthropic_providers.py,sha256=y1_qc8Lbip-YDmpimPGg3DfTm5g-WZk5FrRCXzwF_Ow,2139
21
21
  posthoganalytics/ai/gemini/__init__.py,sha256=bMNBnJ6NO_PCQCwmxKIiw4adFuEQ06hFFBALt-aDW-0,174
22
- posthoganalytics/ai/gemini/gemini.py,sha256=NmVfsG3VaTmDoNCIqXgw2n48H-00zHKfqVY46WFPbEI,13152
22
+ posthoganalytics/ai/gemini/gemini.py,sha256=oi7VIPJLMEHPqRQwvAGwLjkaF0RZhvloCqOJgsQrmJ8,13285
23
23
  posthoganalytics/ai/langchain/__init__.py,sha256=9CqAwLynTGj3ASAR80C3PmdTdrYGmu99tz0JL-HPFgI,70
24
- posthoganalytics/ai/langchain/callbacks.py,sha256=qZlHsD8QJiXbvykBIDvNlPa0lQS4zXaxTmZ0R9JHpDQ,28848
24
+ posthoganalytics/ai/langchain/callbacks.py,sha256=nfLRbAOajQ6DVSCedUUnemY3uS_icTeILEhfnSAUJ4U,28755
25
25
  posthoganalytics/ai/openai/__init__.py,sha256=_flZxkyaDZme9hxJsY31sMlq4nP1dtc75HmNgj-21Kg,197
26
- posthoganalytics/ai/openai/openai.py,sha256=iL_cwctaAhPdXNo4EpIZooOWGyjNj0W-OUEoLchTj9s,23394
27
- posthoganalytics/ai/openai/openai_async.py,sha256=KxPCd5imF5iZ9VkJ12HjCO2skaF1tHsHveAknIqV93g,23769
28
- posthoganalytics/ai/openai/openai_providers.py,sha256=EMuEvdHSOFbrhmfuU0is7pBVWS3ReAUT0PZqgMXdyjk,3884
26
+ posthoganalytics/ai/openai/openai.py,sha256=6bC3OxH9TP7EFkCGQRfxfcormVTAwLP4Wfj3ID3RwEc,23431
27
+ posthoganalytics/ai/openai/openai_async.py,sha256=0gEhTr-ePiOhS8h1WznQDSz_lJm1aferk5K1ZAMo-K0,23838
28
+ posthoganalytics/ai/openai/openai_providers.py,sha256=RPVmj2V0_lAdno_ax5Ul2kwhBA9_rRgAdl_sCqrQc6M,4004
29
29
  posthoganalytics/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  posthoganalytics/integrations/django.py,sha256=KYtBr7CkiZQynRc2TCWWYHe-J3ie8iSUa42WPshYZdc,6795
31
31
  posthoganalytics/test/__init__.py,sha256=VYgM6xPbJbvS-xhIcDiBRs0MFC9V_jT65uNeerCz_rM,299
32
32
  posthoganalytics/test/test_before_send.py,sha256=A1_UVMewhHAvO39rZDWfS606vG_X-q0KNXvh5DAKiB8,7930
33
- posthoganalytics/test/test_client.py,sha256=fghypNGUqOY9OmycsdNK5SsyCoimxYVisFfFq2nkTCU,77840
33
+ posthoganalytics/test/test_client.py,sha256=RJURSog8fX_YEBzv4MGrX1amX0Ma7NUMlwSanfgORmQ,87343
34
34
  posthoganalytics/test/test_consumer.py,sha256=HGMfU9PzQ5ZAe_R3kHnZNsMvD7jUjHL-gie0isrvMMk,7107
35
35
  posthoganalytics/test/test_contexts.py,sha256=c--hNUIEf6SHQ7H9vdPhU1oLCN0SnD4wDbFr-eLPHDo,7013
36
36
  posthoganalytics/test/test_exception_capture.py,sha256=al37Kg6wjzL_IBCFUUXRvkP6nVrqS6IZRCOKSo29Nh8,1063
@@ -42,8 +42,8 @@ posthoganalytics/test/test_request.py,sha256=Zc0VbkjpVmj8mKokQm9rzdgTr0b1U44vvMY
42
42
  posthoganalytics/test/test_size_limited_dict.py,sha256=-5IQjIEr_-Dql24M0HusdR_XroOMrtgiT0v6ZQCRvzo,774
43
43
  posthoganalytics/test/test_types.py,sha256=bRPHdwVpP7hu7emsplU8UVyzSQptv6PaG5lAoOD_BtM,7595
44
44
  posthoganalytics/test/test_utils.py,sha256=sqUTbfweVcxxFRd3WDMFXqPMyU6DvzOBeAOc68Py9aw,9620
45
- posthoganalytics-6.1.1.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
46
- posthoganalytics-6.1.1.dist-info/METADATA,sha256=xrhfLBXcqwgMm1nsVsP5UDPo3nrGC6IZitVt0Dpn5XM,6024
47
- posthoganalytics-6.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
- posthoganalytics-6.1.1.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
49
- posthoganalytics-6.1.1.dist-info/RECORD,,
45
+ posthoganalytics-6.3.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
46
+ posthoganalytics-6.3.0.dist-info/METADATA,sha256=TyFn8t0pOnWbsC11Y3J-iJM50uiDgdQwsmmz9gKmvCw,6024
47
+ posthoganalytics-6.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
+ posthoganalytics-6.3.0.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
49
+ posthoganalytics-6.3.0.dist-info/RECORD,,