posthoganalytics 6.2.1__tar.gz → 6.3.1__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.1}/PKG-INFO +1 -1
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/langchain/callbacks.py +41 -14
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/args.py +6 -3
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/client.py +60 -4
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_client.py +254 -1
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/types.py +18 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/version.py +1 -1
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1/posthoganalytics.egg-info}/PKG-INFO +1 -1
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/LICENSE +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/MANIFEST.in +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/README.md +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/anthropic/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/anthropic/anthropic.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/anthropic/anthropic_async.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/anthropic/anthropic_providers.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/gemini/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/gemini/gemini.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/langchain/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/openai/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/openai/openai.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/openai/openai_async.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/openai/openai_providers.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/utils.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/consumer.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/contexts.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/exception_capture.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/exception_utils.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/feature_flags.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/integrations/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/integrations/django.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/poller.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/py.typed +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/request.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/__init__.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_before_send.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_consumer.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_contexts.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_exception_capture.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_feature_flag.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_feature_flag_result.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_feature_flags.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_module.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_request.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_size_limited_dict.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_types.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_utils.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/utils.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics.egg-info/SOURCES.txt +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics.egg-info/dependency_links.txt +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics.egg-info/requires.txt +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics.egg-info/top_level.txt +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/pyproject.toml +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/setup.cfg +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/setup.py +0 -0
- {posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/setup_analytics.py +0 -0
{posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/ai/langchain/callbacks.py
RENAMED
|
@@ -5,6 +5,7 @@ except ImportError:
|
|
|
5
5
|
"Please install LangChain to use this feature: 'pip install langchain'"
|
|
6
6
|
)
|
|
7
7
|
|
|
8
|
+
import json
|
|
8
9
|
import logging
|
|
9
10
|
import time
|
|
10
11
|
from dataclasses import dataclass
|
|
@@ -29,11 +30,12 @@ from langchain_core.messages import (
|
|
|
29
30
|
HumanMessage,
|
|
30
31
|
SystemMessage,
|
|
31
32
|
ToolMessage,
|
|
33
|
+
ToolCall,
|
|
32
34
|
)
|
|
33
35
|
from langchain_core.outputs import ChatGeneration, LLMResult
|
|
34
36
|
from pydantic import BaseModel
|
|
35
37
|
|
|
36
|
-
from posthoganalytics import
|
|
38
|
+
from posthoganalytics import setup
|
|
37
39
|
from posthoganalytics.ai.utils import get_model_params, with_privacy_mode
|
|
38
40
|
from posthoganalytics.client import Client
|
|
39
41
|
|
|
@@ -81,7 +83,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
81
83
|
The PostHog LLM observability callback handler for LangChain.
|
|
82
84
|
"""
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
_ph_client: Client
|
|
85
87
|
"""PostHog client instance."""
|
|
86
88
|
|
|
87
89
|
_distinct_id: Optional[Union[str, int, UUID]]
|
|
@@ -127,10 +129,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
127
129
|
privacy_mode: Whether to redact the input and output of the trace.
|
|
128
130
|
groups: Optional additional PostHog groups to use for the trace.
|
|
129
131
|
"""
|
|
130
|
-
|
|
131
|
-
if posthog_client is None:
|
|
132
|
-
raise ValueError("PostHog client is required")
|
|
133
|
-
self._client = posthog_client
|
|
132
|
+
self._ph_client = client or setup()
|
|
134
133
|
self._distinct_id = distinct_id
|
|
135
134
|
self._trace_id = trace_id
|
|
136
135
|
self._properties = properties or {}
|
|
@@ -481,7 +480,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
481
480
|
event_properties = {
|
|
482
481
|
"$ai_trace_id": trace_id,
|
|
483
482
|
"$ai_input_state": with_privacy_mode(
|
|
484
|
-
self.
|
|
483
|
+
self._ph_client, self._privacy_mode, run.input
|
|
485
484
|
),
|
|
486
485
|
"$ai_latency": run.latency,
|
|
487
486
|
"$ai_span_name": run.name,
|
|
@@ -497,13 +496,13 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
497
496
|
event_properties["$ai_is_error"] = True
|
|
498
497
|
elif outputs is not None:
|
|
499
498
|
event_properties["$ai_output_state"] = with_privacy_mode(
|
|
500
|
-
self.
|
|
499
|
+
self._ph_client, self._privacy_mode, outputs
|
|
501
500
|
)
|
|
502
501
|
|
|
503
502
|
if self._distinct_id is None:
|
|
504
503
|
event_properties["$process_person_profile"] = False
|
|
505
504
|
|
|
506
|
-
self.
|
|
505
|
+
self._ph_client.capture(
|
|
507
506
|
distinct_id=self._distinct_id or run_id,
|
|
508
507
|
event=event_name,
|
|
509
508
|
properties=event_properties,
|
|
@@ -550,14 +549,16 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
550
549
|
"$ai_provider": run.provider,
|
|
551
550
|
"$ai_model": run.model,
|
|
552
551
|
"$ai_model_parameters": run.model_params,
|
|
553
|
-
"$ai_input": with_privacy_mode(
|
|
552
|
+
"$ai_input": with_privacy_mode(
|
|
553
|
+
self._ph_client, self._privacy_mode, run.input
|
|
554
|
+
),
|
|
554
555
|
"$ai_http_status": 200,
|
|
555
556
|
"$ai_latency": run.latency,
|
|
556
557
|
"$ai_base_url": run.base_url,
|
|
557
558
|
}
|
|
558
559
|
if run.tools:
|
|
559
560
|
event_properties["$ai_tools"] = with_privacy_mode(
|
|
560
|
-
self.
|
|
561
|
+
self._ph_client,
|
|
561
562
|
self._privacy_mode,
|
|
562
563
|
run.tools,
|
|
563
564
|
)
|
|
@@ -589,7 +590,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
589
590
|
_extract_raw_esponse(generation) for generation in generation_result
|
|
590
591
|
]
|
|
591
592
|
event_properties["$ai_output_choices"] = with_privacy_mode(
|
|
592
|
-
self.
|
|
593
|
+
self._ph_client, self._privacy_mode, completions
|
|
593
594
|
)
|
|
594
595
|
|
|
595
596
|
if self._properties:
|
|
@@ -598,7 +599,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
598
599
|
if self._distinct_id is None:
|
|
599
600
|
event_properties["$process_person_profile"] = False
|
|
600
601
|
|
|
601
|
-
self.
|
|
602
|
+
self._ph_client.capture(
|
|
602
603
|
distinct_id=self._distinct_id or trace_id,
|
|
603
604
|
event="$ai_generation",
|
|
604
605
|
properties=event_properties,
|
|
@@ -630,12 +631,35 @@ def _extract_raw_esponse(last_response):
|
|
|
630
631
|
return ""
|
|
631
632
|
|
|
632
633
|
|
|
633
|
-
def
|
|
634
|
+
def _convert_lc_tool_calls_to_oai(
|
|
635
|
+
tool_calls: list[ToolCall],
|
|
636
|
+
) -> list[dict[str, Any]]:
|
|
637
|
+
try:
|
|
638
|
+
return [
|
|
639
|
+
{
|
|
640
|
+
"type": "function",
|
|
641
|
+
"id": tool_call["id"],
|
|
642
|
+
"function": {
|
|
643
|
+
"name": tool_call["name"],
|
|
644
|
+
"arguments": json.dumps(tool_call["args"]),
|
|
645
|
+
},
|
|
646
|
+
}
|
|
647
|
+
for tool_call in tool_calls
|
|
648
|
+
]
|
|
649
|
+
except KeyError:
|
|
650
|
+
return tool_calls
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _convert_message_to_dict(message: BaseMessage) -> dict[str, Any]:
|
|
634
654
|
# assistant message
|
|
635
655
|
if isinstance(message, HumanMessage):
|
|
636
656
|
message_dict = {"role": "user", "content": message.content}
|
|
637
657
|
elif isinstance(message, AIMessage):
|
|
638
658
|
message_dict = {"role": "assistant", "content": message.content}
|
|
659
|
+
if message.tool_calls:
|
|
660
|
+
message_dict["tool_calls"] = _convert_lc_tool_calls_to_oai(
|
|
661
|
+
message.tool_calls
|
|
662
|
+
)
|
|
639
663
|
elif isinstance(message, SystemMessage):
|
|
640
664
|
message_dict = {"role": "system", "content": message.content}
|
|
641
665
|
elif isinstance(message, ToolMessage):
|
|
@@ -648,6 +672,9 @@ def _convert_message_to_dict(message: BaseMessage) -> Dict[str, Any]:
|
|
|
648
672
|
if message.additional_kwargs:
|
|
649
673
|
message_dict.update(message.additional_kwargs)
|
|
650
674
|
|
|
675
|
+
if "content" in message_dict and not message_dict["content"]:
|
|
676
|
+
message_dict["content"] = ""
|
|
677
|
+
|
|
651
678
|
return message_dict
|
|
652
679
|
|
|
653
680
|
|
|
@@ -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.1}/posthoganalytics/ai/anthropic/anthropic.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/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.1}/posthoganalytics/ai/openai/openai_async.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/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.1}/posthoganalytics/test/test_exception_capture.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_feature_flag.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_feature_flag_result.py
RENAMED
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/posthoganalytics/test/test_feature_flags.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.2.1 → posthoganalytics-6.3.1}/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.1}/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
|