posthoganalytics 7.5.0__tar.gz → 7.6.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-7.5.0/posthoganalytics.egg-info → posthoganalytics-7.6.0}/PKG-INFO +9 -1
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/README.md +8 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/__init__.py +35 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/client.py +38 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/consumer.py +6 -2
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/contexts.py +44 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_client.py +115 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_consumer.py +86 -83
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/version.py +1 -1
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0/posthoganalytics.egg-info}/PKG-INFO +9 -1
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/LICENSE +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/MANIFEST.in +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/__init__.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/anthropic/__init__.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/anthropic/anthropic.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/anthropic/anthropic_async.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/anthropic/anthropic_converter.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/anthropic/anthropic_providers.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/gemini/__init__.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/gemini/gemini.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/gemini/gemini_async.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/gemini/gemini_converter.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/langchain/__init__.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/langchain/callbacks.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/openai/__init__.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/openai/openai.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/openai/openai_async.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/openai/openai_converter.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/openai/openai_providers.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/sanitization.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/types.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/utils.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/args.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/exception_capture.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/exception_utils.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/feature_flags.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/flag_definition_cache.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/integrations/__init__.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/integrations/django.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/poller.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/py.typed +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/request.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/__init__.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_before_send.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_contexts.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_exception_capture.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_feature_flag.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_feature_flag_result.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_feature_flags.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_flag_definition_cache.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_module.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_request.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_size_limited_dict.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_types.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_utils.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/types.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/utils.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics.egg-info/SOURCES.txt +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics.egg-info/dependency_links.txt +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics.egg-info/requires.txt +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics.egg-info/top_level.txt +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/pyproject.toml +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/setup.cfg +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/setup.py +0 -0
- {posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/setup_analytics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: posthoganalytics
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.6.0
|
|
4
4
|
Summary: Integrate PostHog into any python application.
|
|
5
5
|
Home-page: https://github.com/posthog/posthog-python
|
|
6
6
|
Author: Posthog
|
|
@@ -87,6 +87,14 @@ Dynamic: maintainer
|
|
|
87
87
|
|
|
88
88
|
Please see the [Python integration docs](https://posthog.com/docs/integrations/python-integration) for details.
|
|
89
89
|
|
|
90
|
+
## Python Version Support
|
|
91
|
+
|
|
92
|
+
| SDK Version | Python Versions Supported | Notes |
|
|
93
|
+
|-------------|---------------------------|-------|
|
|
94
|
+
| 7.3.1+ | 3.10, 3.11, 3.12, 3.13, 3.14 | Added Python 3.14 support |
|
|
95
|
+
| 7.0.0 - 7.0.1 | 3.10, 3.11, 3.12, 3.13 | Dropped Python 3.9 support |
|
|
96
|
+
| 4.0.1 - 6.x | 3.9, 3.10, 3.11, 3.12, 3.13 | Python 3.9+ required |
|
|
97
|
+
|
|
90
98
|
## Development
|
|
91
99
|
|
|
92
100
|
### Testing Locally
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
Please see the [Python integration docs](https://posthog.com/docs/integrations/python-integration) for details.
|
|
14
14
|
|
|
15
|
+
## Python Version Support
|
|
16
|
+
|
|
17
|
+
| SDK Version | Python Versions Supported | Notes |
|
|
18
|
+
|-------------|---------------------------|-------|
|
|
19
|
+
| 7.3.1+ | 3.10, 3.11, 3.12, 3.13, 3.14 | Added Python 3.14 support |
|
|
20
|
+
| 7.0.0 - 7.0.1 | 3.10, 3.11, 3.12, 3.13 | Dropped Python 3.9 support |
|
|
21
|
+
| 4.0.1 - 6.x | 3.9, 3.10, 3.11, 3.12, 3.13 | Python 3.9+ required |
|
|
22
|
+
|
|
15
23
|
## Development
|
|
16
24
|
|
|
17
25
|
### Testing Locally
|
|
@@ -23,6 +23,9 @@ from posthoganalytics.contexts import (
|
|
|
23
23
|
from posthoganalytics.contexts import (
|
|
24
24
|
set_code_variables_mask_patterns_context as inner_set_code_variables_mask_patterns_context,
|
|
25
25
|
)
|
|
26
|
+
from posthoganalytics.contexts import (
|
|
27
|
+
set_context_device_id as inner_set_context_device_id,
|
|
28
|
+
)
|
|
26
29
|
from posthoganalytics.contexts import (
|
|
27
30
|
set_context_session as inner_set_context_session,
|
|
28
31
|
)
|
|
@@ -133,6 +136,26 @@ def set_context_session(session_id: str):
|
|
|
133
136
|
return inner_set_context_session(session_id)
|
|
134
137
|
|
|
135
138
|
|
|
139
|
+
def set_context_device_id(device_id: str):
|
|
140
|
+
"""
|
|
141
|
+
Set the device ID for the current context, associating all feature flag requests
|
|
142
|
+
in this or child contexts with the given device ID.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
device_id: The device ID to associate with the current context and its children
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
```python
|
|
149
|
+
from posthoganalytics import set_context_device_id
|
|
150
|
+
set_context_device_id("device_123")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Category:
|
|
154
|
+
Contexts
|
|
155
|
+
"""
|
|
156
|
+
return inner_set_context_device_id(device_id)
|
|
157
|
+
|
|
158
|
+
|
|
136
159
|
def identify_context(distinct_id: str):
|
|
137
160
|
"""
|
|
138
161
|
Identify the current context with a distinct ID.
|
|
@@ -483,6 +506,7 @@ def feature_enabled(
|
|
|
483
506
|
only_evaluate_locally=False, # type: bool
|
|
484
507
|
send_feature_flag_events=True, # type: bool
|
|
485
508
|
disable_geoip=None, # type: Optional[bool]
|
|
509
|
+
device_id=None, # type: Optional[str]
|
|
486
510
|
):
|
|
487
511
|
# type: (...) -> bool
|
|
488
512
|
"""
|
|
@@ -522,6 +546,7 @@ def feature_enabled(
|
|
|
522
546
|
only_evaluate_locally=only_evaluate_locally,
|
|
523
547
|
send_feature_flag_events=send_feature_flag_events,
|
|
524
548
|
disable_geoip=disable_geoip,
|
|
549
|
+
device_id=device_id,
|
|
525
550
|
)
|
|
526
551
|
|
|
527
552
|
|
|
@@ -534,6 +559,7 @@ def get_feature_flag(
|
|
|
534
559
|
only_evaluate_locally=False, # type: bool
|
|
535
560
|
send_feature_flag_events=True, # type: bool
|
|
536
561
|
disable_geoip=None, # type: Optional[bool]
|
|
562
|
+
device_id=None, # type: Optional[str]
|
|
537
563
|
) -> Optional[FeatureFlag]:
|
|
538
564
|
"""
|
|
539
565
|
Get feature flag variant for users. Used with experiments.
|
|
@@ -572,6 +598,7 @@ def get_feature_flag(
|
|
|
572
598
|
only_evaluate_locally=only_evaluate_locally,
|
|
573
599
|
send_feature_flag_events=send_feature_flag_events,
|
|
574
600
|
disable_geoip=disable_geoip,
|
|
601
|
+
device_id=device_id,
|
|
575
602
|
)
|
|
576
603
|
|
|
577
604
|
|
|
@@ -582,6 +609,7 @@ def get_all_flags(
|
|
|
582
609
|
group_properties=None, # type: Optional[dict]
|
|
583
610
|
only_evaluate_locally=False, # type: bool
|
|
584
611
|
disable_geoip=None, # type: Optional[bool]
|
|
612
|
+
device_id=None, # type: Optional[str]
|
|
585
613
|
) -> Optional[dict[str, FeatureFlag]]:
|
|
586
614
|
"""
|
|
587
615
|
Get all flags for a given user.
|
|
@@ -614,6 +642,7 @@ def get_all_flags(
|
|
|
614
642
|
group_properties=group_properties or {},
|
|
615
643
|
only_evaluate_locally=only_evaluate_locally,
|
|
616
644
|
disable_geoip=disable_geoip,
|
|
645
|
+
device_id=device_id,
|
|
617
646
|
)
|
|
618
647
|
|
|
619
648
|
|
|
@@ -626,6 +655,7 @@ def get_feature_flag_result(
|
|
|
626
655
|
only_evaluate_locally=False,
|
|
627
656
|
send_feature_flag_events=True,
|
|
628
657
|
disable_geoip=None, # type: Optional[bool]
|
|
658
|
+
device_id=None, # type: Optional[str]
|
|
629
659
|
):
|
|
630
660
|
# type: (...) -> Optional[FeatureFlagResult]
|
|
631
661
|
"""
|
|
@@ -657,6 +687,7 @@ def get_feature_flag_result(
|
|
|
657
687
|
only_evaluate_locally=only_evaluate_locally,
|
|
658
688
|
send_feature_flag_events=send_feature_flag_events,
|
|
659
689
|
disable_geoip=disable_geoip,
|
|
690
|
+
device_id=device_id,
|
|
660
691
|
)
|
|
661
692
|
|
|
662
693
|
|
|
@@ -670,6 +701,7 @@ def get_feature_flag_payload(
|
|
|
670
701
|
only_evaluate_locally=False,
|
|
671
702
|
send_feature_flag_events=True,
|
|
672
703
|
disable_geoip=None, # type: Optional[bool]
|
|
704
|
+
device_id=None, # type: Optional[str]
|
|
673
705
|
) -> Optional[str]:
|
|
674
706
|
return _proxy(
|
|
675
707
|
"get_feature_flag_payload",
|
|
@@ -682,6 +714,7 @@ def get_feature_flag_payload(
|
|
|
682
714
|
only_evaluate_locally=only_evaluate_locally,
|
|
683
715
|
send_feature_flag_events=send_feature_flag_events,
|
|
684
716
|
disable_geoip=disable_geoip,
|
|
717
|
+
device_id=device_id,
|
|
685
718
|
)
|
|
686
719
|
|
|
687
720
|
|
|
@@ -712,6 +745,7 @@ def get_all_flags_and_payloads(
|
|
|
712
745
|
group_properties=None, # type: Optional[dict]
|
|
713
746
|
only_evaluate_locally=False,
|
|
714
747
|
disable_geoip=None, # type: Optional[bool]
|
|
748
|
+
device_id=None, # type: Optional[str]
|
|
715
749
|
) -> FlagsAndPayloads:
|
|
716
750
|
return _proxy(
|
|
717
751
|
"get_all_flags_and_payloads",
|
|
@@ -721,6 +755,7 @@ def get_all_flags_and_payloads(
|
|
|
721
755
|
group_properties=group_properties or {},
|
|
722
756
|
only_evaluate_locally=only_evaluate_locally,
|
|
723
757
|
disable_geoip=disable_geoip,
|
|
758
|
+
device_id=device_id,
|
|
724
759
|
)
|
|
725
760
|
|
|
726
761
|
|
|
@@ -18,6 +18,7 @@ from posthoganalytics.contexts import (
|
|
|
18
18
|
get_capture_exception_code_variables_context,
|
|
19
19
|
get_code_variables_ignore_patterns_context,
|
|
20
20
|
get_code_variables_mask_patterns_context,
|
|
21
|
+
get_context_device_id,
|
|
21
22
|
get_context_distinct_id,
|
|
22
23
|
get_context_session_id,
|
|
23
24
|
new_context,
|
|
@@ -382,6 +383,7 @@ class Client(object):
|
|
|
382
383
|
group_properties=None,
|
|
383
384
|
disable_geoip=None,
|
|
384
385
|
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
386
|
+
device_id: Optional[str] = None,
|
|
385
387
|
) -> dict[str, Union[bool, str]]:
|
|
386
388
|
"""
|
|
387
389
|
Get feature flag variants for a user by calling decide.
|
|
@@ -394,6 +396,7 @@ class Client(object):
|
|
|
394
396
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
395
397
|
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
396
398
|
only these flags will be evaluated, improving performance.
|
|
399
|
+
device_id: The device ID for this request.
|
|
397
400
|
|
|
398
401
|
Category:
|
|
399
402
|
Feature flags
|
|
@@ -405,6 +408,7 @@ class Client(object):
|
|
|
405
408
|
group_properties,
|
|
406
409
|
disable_geoip,
|
|
407
410
|
flag_keys_to_evaluate,
|
|
411
|
+
device_id=device_id,
|
|
408
412
|
)
|
|
409
413
|
return to_values(resp_data) or {}
|
|
410
414
|
|
|
@@ -416,6 +420,7 @@ class Client(object):
|
|
|
416
420
|
group_properties=None,
|
|
417
421
|
disable_geoip=None,
|
|
418
422
|
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
423
|
+
device_id: Optional[str] = None,
|
|
419
424
|
) -> dict[str, str]:
|
|
420
425
|
"""
|
|
421
426
|
Get feature flag payloads for a user by calling decide.
|
|
@@ -428,6 +433,7 @@ class Client(object):
|
|
|
428
433
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
429
434
|
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
430
435
|
only these flags will be evaluated, improving performance.
|
|
436
|
+
device_id: The device ID for this request.
|
|
431
437
|
|
|
432
438
|
Examples:
|
|
433
439
|
```python
|
|
@@ -444,6 +450,7 @@ class Client(object):
|
|
|
444
450
|
group_properties,
|
|
445
451
|
disable_geoip,
|
|
446
452
|
flag_keys_to_evaluate,
|
|
453
|
+
device_id=device_id,
|
|
447
454
|
)
|
|
448
455
|
return to_payloads(resp_data) or {}
|
|
449
456
|
|
|
@@ -455,6 +462,7 @@ class Client(object):
|
|
|
455
462
|
group_properties=None,
|
|
456
463
|
disable_geoip=None,
|
|
457
464
|
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
465
|
+
device_id: Optional[str] = None,
|
|
458
466
|
) -> FlagsAndPayloads:
|
|
459
467
|
"""
|
|
460
468
|
Get feature flags and payloads for a user by calling decide.
|
|
@@ -467,6 +475,7 @@ class Client(object):
|
|
|
467
475
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
468
476
|
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
469
477
|
only these flags will be evaluated, improving performance.
|
|
478
|
+
device_id: The device ID for this request.
|
|
470
479
|
|
|
471
480
|
Examples:
|
|
472
481
|
```python
|
|
@@ -483,6 +492,7 @@ class Client(object):
|
|
|
483
492
|
group_properties,
|
|
484
493
|
disable_geoip,
|
|
485
494
|
flag_keys_to_evaluate,
|
|
495
|
+
device_id=device_id,
|
|
486
496
|
)
|
|
487
497
|
return to_flags_and_payloads(resp)
|
|
488
498
|
|
|
@@ -494,6 +504,7 @@ class Client(object):
|
|
|
494
504
|
group_properties=None,
|
|
495
505
|
disable_geoip=None,
|
|
496
506
|
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
507
|
+
device_id: Optional[str] = None,
|
|
497
508
|
) -> FlagsResponse:
|
|
498
509
|
"""
|
|
499
510
|
Get feature flags decision.
|
|
@@ -506,6 +517,7 @@ class Client(object):
|
|
|
506
517
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
507
518
|
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
508
519
|
only these flags will be evaluated, improving performance.
|
|
520
|
+
device_id: The device ID for this request.
|
|
509
521
|
|
|
510
522
|
Examples:
|
|
511
523
|
```python
|
|
@@ -522,6 +534,9 @@ class Client(object):
|
|
|
522
534
|
if distinct_id is None:
|
|
523
535
|
distinct_id = get_context_distinct_id()
|
|
524
536
|
|
|
537
|
+
if device_id is None:
|
|
538
|
+
device_id = get_context_device_id()
|
|
539
|
+
|
|
525
540
|
if disable_geoip is None:
|
|
526
541
|
disable_geoip = self.disable_geoip
|
|
527
542
|
|
|
@@ -534,6 +549,7 @@ class Client(object):
|
|
|
534
549
|
"person_properties": person_properties,
|
|
535
550
|
"group_properties": group_properties,
|
|
536
551
|
"geoip_disable": disable_geoip,
|
|
552
|
+
"device_id": device_id,
|
|
537
553
|
}
|
|
538
554
|
|
|
539
555
|
if flag_keys_to_evaluate:
|
|
@@ -1464,6 +1480,7 @@ class Client(object):
|
|
|
1464
1480
|
only_evaluate_locally=False,
|
|
1465
1481
|
send_feature_flag_events=True,
|
|
1466
1482
|
disable_geoip=None,
|
|
1483
|
+
device_id: Optional[str] = None,
|
|
1467
1484
|
):
|
|
1468
1485
|
"""
|
|
1469
1486
|
Check if a feature flag is enabled for a user.
|
|
@@ -1477,6 +1494,7 @@ class Client(object):
|
|
|
1477
1494
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1478
1495
|
send_feature_flag_events: Whether to send feature flag events.
|
|
1479
1496
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
1497
|
+
device_id: The device ID for this request.
|
|
1480
1498
|
|
|
1481
1499
|
Examples:
|
|
1482
1500
|
```python
|
|
@@ -1499,6 +1517,7 @@ class Client(object):
|
|
|
1499
1517
|
only_evaluate_locally=only_evaluate_locally,
|
|
1500
1518
|
send_feature_flag_events=send_feature_flag_events,
|
|
1501
1519
|
disable_geoip=disable_geoip,
|
|
1520
|
+
device_id=device_id,
|
|
1502
1521
|
)
|
|
1503
1522
|
|
|
1504
1523
|
if response is None:
|
|
@@ -1530,6 +1549,7 @@ class Client(object):
|
|
|
1530
1549
|
only_evaluate_locally=False,
|
|
1531
1550
|
send_feature_flag_events=True,
|
|
1532
1551
|
disable_geoip=None,
|
|
1552
|
+
device_id: Optional[str] = None,
|
|
1533
1553
|
) -> Optional[FeatureFlagResult]:
|
|
1534
1554
|
if self.disabled:
|
|
1535
1555
|
return None
|
|
@@ -1584,6 +1604,7 @@ class Client(object):
|
|
|
1584
1604
|
person_properties,
|
|
1585
1605
|
group_properties,
|
|
1586
1606
|
disable_geoip,
|
|
1607
|
+
device_id=device_id,
|
|
1587
1608
|
)
|
|
1588
1609
|
)
|
|
1589
1610
|
errors = []
|
|
@@ -1656,6 +1677,7 @@ class Client(object):
|
|
|
1656
1677
|
only_evaluate_locally=False,
|
|
1657
1678
|
send_feature_flag_events=True,
|
|
1658
1679
|
disable_geoip=None,
|
|
1680
|
+
device_id: Optional[str] = None,
|
|
1659
1681
|
) -> Optional[FeatureFlagResult]:
|
|
1660
1682
|
"""
|
|
1661
1683
|
Get a FeatureFlagResult object which contains the flag result and payload for a key by evaluating locally or remotely
|
|
@@ -1680,6 +1702,7 @@ class Client(object):
|
|
|
1680
1702
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1681
1703
|
send_feature_flag_events: Whether to send feature flag events.
|
|
1682
1704
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
1705
|
+
device_id: The device ID for this request.
|
|
1683
1706
|
|
|
1684
1707
|
Returns:
|
|
1685
1708
|
Optional[FeatureFlagResult]: The feature flag result or None if disabled/not found.
|
|
@@ -1693,6 +1716,7 @@ class Client(object):
|
|
|
1693
1716
|
only_evaluate_locally=only_evaluate_locally,
|
|
1694
1717
|
send_feature_flag_events=send_feature_flag_events,
|
|
1695
1718
|
disable_geoip=disable_geoip,
|
|
1719
|
+
device_id=device_id,
|
|
1696
1720
|
)
|
|
1697
1721
|
|
|
1698
1722
|
def get_feature_flag(
|
|
@@ -1706,6 +1730,7 @@ class Client(object):
|
|
|
1706
1730
|
only_evaluate_locally=False,
|
|
1707
1731
|
send_feature_flag_events=True,
|
|
1708
1732
|
disable_geoip=None,
|
|
1733
|
+
device_id: Optional[str] = None,
|
|
1709
1734
|
) -> Optional[FlagValue]:
|
|
1710
1735
|
"""
|
|
1711
1736
|
Get multivariate feature flag value for a user.
|
|
@@ -1719,6 +1744,7 @@ class Client(object):
|
|
|
1719
1744
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1720
1745
|
send_feature_flag_events: Whether to send feature flag events.
|
|
1721
1746
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
1747
|
+
device_id: The device ID for this request.
|
|
1722
1748
|
|
|
1723
1749
|
Examples:
|
|
1724
1750
|
```python
|
|
@@ -1741,6 +1767,7 @@ class Client(object):
|
|
|
1741
1767
|
only_evaluate_locally=only_evaluate_locally,
|
|
1742
1768
|
send_feature_flag_events=send_feature_flag_events,
|
|
1743
1769
|
disable_geoip=disable_geoip,
|
|
1770
|
+
device_id=device_id,
|
|
1744
1771
|
)
|
|
1745
1772
|
return feature_flag_result.get_value() if feature_flag_result else None
|
|
1746
1773
|
|
|
@@ -1794,6 +1821,7 @@ class Client(object):
|
|
|
1794
1821
|
only_evaluate_locally=False,
|
|
1795
1822
|
send_feature_flag_events=False,
|
|
1796
1823
|
disable_geoip=None,
|
|
1824
|
+
device_id: Optional[str] = None,
|
|
1797
1825
|
):
|
|
1798
1826
|
"""
|
|
1799
1827
|
Get the payload for a feature flag.
|
|
@@ -1808,6 +1836,7 @@ class Client(object):
|
|
|
1808
1836
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1809
1837
|
send_feature_flag_events: Deprecated. Use get_feature_flag() instead if you need events.
|
|
1810
1838
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
1839
|
+
device_id: The device ID for this request.
|
|
1811
1840
|
|
|
1812
1841
|
Examples:
|
|
1813
1842
|
```python
|
|
@@ -1840,6 +1869,7 @@ class Client(object):
|
|
|
1840
1869
|
only_evaluate_locally=only_evaluate_locally,
|
|
1841
1870
|
send_feature_flag_events=send_feature_flag_events,
|
|
1842
1871
|
disable_geoip=disable_geoip,
|
|
1872
|
+
device_id=device_id,
|
|
1843
1873
|
)
|
|
1844
1874
|
return feature_flag_result.payload if feature_flag_result else None
|
|
1845
1875
|
|
|
@@ -1851,6 +1881,7 @@ class Client(object):
|
|
|
1851
1881
|
person_properties: dict[str, str],
|
|
1852
1882
|
group_properties: dict[str, str],
|
|
1853
1883
|
disable_geoip: Optional[bool],
|
|
1884
|
+
device_id: Optional[str] = None,
|
|
1854
1885
|
) -> tuple[Optional[FeatureFlag], Optional[str], Optional[int], bool]:
|
|
1855
1886
|
"""
|
|
1856
1887
|
Calls /flags and returns the flag details, request id, evaluated at timestamp,
|
|
@@ -1863,6 +1894,7 @@ class Client(object):
|
|
|
1863
1894
|
group_properties,
|
|
1864
1895
|
disable_geoip,
|
|
1865
1896
|
flag_keys_to_evaluate=[key],
|
|
1897
|
+
device_id=device_id,
|
|
1866
1898
|
)
|
|
1867
1899
|
request_id = resp_data.get("requestId")
|
|
1868
1900
|
evaluated_at = resp_data.get("evaluatedAt")
|
|
@@ -1987,6 +2019,7 @@ class Client(object):
|
|
|
1987
2019
|
only_evaluate_locally=False,
|
|
1988
2020
|
disable_geoip=None,
|
|
1989
2021
|
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
2022
|
+
device_id: Optional[str] = None,
|
|
1990
2023
|
) -> Optional[dict[str, Union[bool, str]]]:
|
|
1991
2024
|
"""
|
|
1992
2025
|
Get all feature flags for a user.
|
|
@@ -2000,6 +2033,7 @@ class Client(object):
|
|
|
2000
2033
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
2001
2034
|
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
2002
2035
|
only these flags will be evaluated, improving performance.
|
|
2036
|
+
device_id: The device ID for this request.
|
|
2003
2037
|
|
|
2004
2038
|
Examples:
|
|
2005
2039
|
```python
|
|
@@ -2017,6 +2051,7 @@ class Client(object):
|
|
|
2017
2051
|
only_evaluate_locally=only_evaluate_locally,
|
|
2018
2052
|
disable_geoip=disable_geoip,
|
|
2019
2053
|
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
2054
|
+
device_id=device_id,
|
|
2020
2055
|
)
|
|
2021
2056
|
|
|
2022
2057
|
return response["featureFlags"]
|
|
@@ -2031,6 +2066,7 @@ class Client(object):
|
|
|
2031
2066
|
only_evaluate_locally=False,
|
|
2032
2067
|
disable_geoip=None,
|
|
2033
2068
|
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
2069
|
+
device_id: Optional[str] = None,
|
|
2034
2070
|
) -> FlagsAndPayloads:
|
|
2035
2071
|
"""
|
|
2036
2072
|
Get all feature flags and their payloads for a user.
|
|
@@ -2044,6 +2080,7 @@ class Client(object):
|
|
|
2044
2080
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
2045
2081
|
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
2046
2082
|
only these flags will be evaluated, improving performance.
|
|
2083
|
+
device_id: The device ID for this request.
|
|
2047
2084
|
|
|
2048
2085
|
Examples:
|
|
2049
2086
|
```python
|
|
@@ -2079,6 +2116,7 @@ class Client(object):
|
|
|
2079
2116
|
group_properties=group_properties,
|
|
2080
2117
|
disable_geoip=disable_geoip,
|
|
2081
2118
|
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
2119
|
+
device_id=device_id,
|
|
2082
2120
|
)
|
|
2083
2121
|
return to_flags_and_payloads(decide_response)
|
|
2084
2122
|
except Exception as e:
|
|
@@ -84,12 +84,16 @@ class Consumer(Thread):
|
|
|
84
84
|
self.log.error("error uploading: %s", e)
|
|
85
85
|
success = False
|
|
86
86
|
if self.on_error:
|
|
87
|
-
|
|
87
|
+
try:
|
|
88
|
+
self.on_error(e, batch)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
self.log.error("on_error handler failed: %s", e)
|
|
88
91
|
finally:
|
|
89
92
|
# mark items as acknowledged from queue
|
|
90
93
|
for item in batch:
|
|
91
94
|
self.queue.task_done()
|
|
92
|
-
|
|
95
|
+
|
|
96
|
+
return success
|
|
93
97
|
|
|
94
98
|
def next(self):
|
|
95
99
|
"""Return the next batch of items to upload."""
|
|
@@ -21,6 +21,7 @@ class ContextScope:
|
|
|
21
21
|
self.capture_exceptions = capture_exceptions
|
|
22
22
|
self.session_id: Optional[str] = None
|
|
23
23
|
self.distinct_id: Optional[str] = None
|
|
24
|
+
self.device_id: Optional[str] = None
|
|
24
25
|
self.tags: Dict[str, Any] = {}
|
|
25
26
|
self.capture_exception_code_variables: Optional[bool] = None
|
|
26
27
|
self.code_variables_mask_patterns: Optional[list] = None
|
|
@@ -32,6 +33,9 @@ class ContextScope:
|
|
|
32
33
|
def set_distinct_id(self, distinct_id: str):
|
|
33
34
|
self.distinct_id = distinct_id
|
|
34
35
|
|
|
36
|
+
def set_device_id(self, device_id: str):
|
|
37
|
+
self.device_id = device_id
|
|
38
|
+
|
|
35
39
|
def add_tag(self, key: str, value: Any):
|
|
36
40
|
self.tags[key] = value
|
|
37
41
|
|
|
@@ -61,6 +65,13 @@ class ContextScope:
|
|
|
61
65
|
return self.parent.get_distinct_id()
|
|
62
66
|
return None
|
|
63
67
|
|
|
68
|
+
def get_device_id(self) -> Optional[str]:
|
|
69
|
+
if self.device_id is not None:
|
|
70
|
+
return self.device_id
|
|
71
|
+
if self.parent is not None and not self.fresh:
|
|
72
|
+
return self.parent.get_device_id()
|
|
73
|
+
return None
|
|
74
|
+
|
|
64
75
|
def collect_tags(self) -> Dict[str, Any]:
|
|
65
76
|
if self.parent and not self.fresh:
|
|
66
77
|
# We want child tags to take precedence over parent tags,
|
|
@@ -275,6 +286,39 @@ def get_context_distinct_id() -> Optional[str]:
|
|
|
275
286
|
return None
|
|
276
287
|
|
|
277
288
|
|
|
289
|
+
def set_context_device_id(device_id: str) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Set the device ID for the current context, associating all feature flag requests in this or
|
|
292
|
+
child contexts with the given device ID (unless set_context_device_id is called again).
|
|
293
|
+
Entering a fresh context will clear the context-level device ID.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
device_id: The device ID to associate with the current context and its children.
|
|
297
|
+
|
|
298
|
+
Category:
|
|
299
|
+
Contexts
|
|
300
|
+
"""
|
|
301
|
+
current_context = _get_current_context()
|
|
302
|
+
if current_context:
|
|
303
|
+
current_context.set_device_id(device_id)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def get_context_device_id() -> Optional[str]:
|
|
307
|
+
"""
|
|
308
|
+
Get the device ID for the current context.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
The device ID if set, None otherwise
|
|
312
|
+
|
|
313
|
+
Category:
|
|
314
|
+
Contexts
|
|
315
|
+
"""
|
|
316
|
+
current_context = _get_current_context()
|
|
317
|
+
if current_context:
|
|
318
|
+
return current_context.get_device_id()
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
278
322
|
def set_capture_exception_code_variables_context(enabled: bool) -> None:
|
|
279
323
|
"""
|
|
280
324
|
Set whether code variables are captured for the current context.
|
|
@@ -648,6 +648,7 @@ class TestClient(unittest.TestCase):
|
|
|
648
648
|
person_properties={},
|
|
649
649
|
group_properties={},
|
|
650
650
|
geoip_disable=True,
|
|
651
|
+
device_id=None,
|
|
651
652
|
)
|
|
652
653
|
|
|
653
654
|
@mock.patch("posthog.client.flags")
|
|
@@ -712,6 +713,7 @@ class TestClient(unittest.TestCase):
|
|
|
712
713
|
person_properties={},
|
|
713
714
|
group_properties={},
|
|
714
715
|
geoip_disable=False,
|
|
716
|
+
device_id=None,
|
|
715
717
|
)
|
|
716
718
|
|
|
717
719
|
@mock.patch("posthog.client.flags")
|
|
@@ -1911,6 +1913,7 @@ class TestClient(unittest.TestCase):
|
|
|
1911
1913
|
person_properties={"distinct_id": "some_id"},
|
|
1912
1914
|
group_properties={},
|
|
1913
1915
|
geoip_disable=True,
|
|
1916
|
+
device_id=None,
|
|
1914
1917
|
flag_keys_to_evaluate=["random_key"],
|
|
1915
1918
|
)
|
|
1916
1919
|
patch_flags.reset_mock()
|
|
@@ -1926,6 +1929,7 @@ class TestClient(unittest.TestCase):
|
|
|
1926
1929
|
person_properties={"distinct_id": "feature_enabled_distinct_id"},
|
|
1927
1930
|
group_properties={},
|
|
1928
1931
|
geoip_disable=True,
|
|
1932
|
+
device_id=None,
|
|
1929
1933
|
flag_keys_to_evaluate=["random_key"],
|
|
1930
1934
|
)
|
|
1931
1935
|
patch_flags.reset_mock()
|
|
@@ -1939,6 +1943,7 @@ class TestClient(unittest.TestCase):
|
|
|
1939
1943
|
person_properties={"distinct_id": "all_flags_payloads_id"},
|
|
1940
1944
|
group_properties={},
|
|
1941
1945
|
geoip_disable=False,
|
|
1946
|
+
device_id=None,
|
|
1942
1947
|
)
|
|
1943
1948
|
|
|
1944
1949
|
@mock.patch("posthog.client.Poller")
|
|
@@ -1987,6 +1992,7 @@ class TestClient(unittest.TestCase):
|
|
|
1987
1992
|
"instance": {"$group_key": "app.posthog.com"},
|
|
1988
1993
|
},
|
|
1989
1994
|
geoip_disable=False,
|
|
1995
|
+
device_id=None,
|
|
1990
1996
|
flag_keys_to_evaluate=["random_key"],
|
|
1991
1997
|
)
|
|
1992
1998
|
|
|
@@ -2014,6 +2020,7 @@ class TestClient(unittest.TestCase):
|
|
|
2014
2020
|
"instance": {"$group_key": "app.posthog.com"},
|
|
2015
2021
|
},
|
|
2016
2022
|
geoip_disable=False,
|
|
2023
|
+
device_id=None,
|
|
2017
2024
|
flag_keys_to_evaluate=["random_key"],
|
|
2018
2025
|
)
|
|
2019
2026
|
|
|
@@ -2031,8 +2038,116 @@ class TestClient(unittest.TestCase):
|
|
|
2031
2038
|
person_properties={"distinct_id": "some_id"},
|
|
2032
2039
|
group_properties={},
|
|
2033
2040
|
geoip_disable=False,
|
|
2041
|
+
device_id=None,
|
|
2034
2042
|
)
|
|
2035
2043
|
|
|
2044
|
+
@parameterized.expand(
|
|
2045
|
+
[
|
|
2046
|
+
# method, method_args, expected_person_props, expected_flag_keys
|
|
2047
|
+
(
|
|
2048
|
+
"get_feature_flag",
|
|
2049
|
+
["random_key", "some_id"],
|
|
2050
|
+
{"distinct_id": "some_id"},
|
|
2051
|
+
["random_key"],
|
|
2052
|
+
),
|
|
2053
|
+
(
|
|
2054
|
+
"feature_enabled",
|
|
2055
|
+
["random_key", "some_id"],
|
|
2056
|
+
{"distinct_id": "some_id"},
|
|
2057
|
+
["random_key"],
|
|
2058
|
+
),
|
|
2059
|
+
(
|
|
2060
|
+
"get_all_flags_and_payloads",
|
|
2061
|
+
["some_id"],
|
|
2062
|
+
{"distinct_id": "some_id"},
|
|
2063
|
+
None,
|
|
2064
|
+
),
|
|
2065
|
+
("get_all_flags", ["some_id"], {"distinct_id": "some_id"}, None),
|
|
2066
|
+
("get_flags_decision", ["some_id"], {}, None),
|
|
2067
|
+
]
|
|
2068
|
+
)
|
|
2069
|
+
@mock.patch("posthog.client.flags")
|
|
2070
|
+
def test_device_id_is_passed_to_flags_request(
|
|
2071
|
+
self,
|
|
2072
|
+
method,
|
|
2073
|
+
method_args,
|
|
2074
|
+
expected_person_props,
|
|
2075
|
+
expected_flag_keys,
|
|
2076
|
+
patch_flags,
|
|
2077
|
+
):
|
|
2078
|
+
"""Test that device_id is properly passed to the flags request when provided."""
|
|
2079
|
+
patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
|
|
2080
|
+
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
|
|
2081
|
+
|
|
2082
|
+
getattr(client, method)(*method_args, device_id="test-device-123")
|
|
2083
|
+
|
|
2084
|
+
expected_call = {
|
|
2085
|
+
"distinct_id": "some_id",
|
|
2086
|
+
"groups": {},
|
|
2087
|
+
"person_properties": expected_person_props,
|
|
2088
|
+
"group_properties": {},
|
|
2089
|
+
"geoip_disable": True,
|
|
2090
|
+
"device_id": "test-device-123",
|
|
2091
|
+
}
|
|
2092
|
+
if expected_flag_keys:
|
|
2093
|
+
expected_call["flag_keys_to_evaluate"] = expected_flag_keys
|
|
2094
|
+
|
|
2095
|
+
patch_flags.assert_called_with(
|
|
2096
|
+
"random_key", "https://us.i.posthog.com", timeout=3, **expected_call
|
|
2097
|
+
)
|
|
2098
|
+
|
|
2099
|
+
@mock.patch("posthog.client.flags")
|
|
2100
|
+
def test_device_id_from_context_is_used_in_flags_request(self, patch_flags):
|
|
2101
|
+
"""Test that device_id from context is used in flags request when not explicitly provided."""
|
|
2102
|
+
from posthoganalytics.contexts import new_context, set_context_device_id
|
|
2103
|
+
|
|
2104
|
+
patch_flags.return_value = {
|
|
2105
|
+
"featureFlags": {
|
|
2106
|
+
"beta-feature": "random-variant",
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
client = Client(
|
|
2110
|
+
FAKE_TEST_API_KEY,
|
|
2111
|
+
on_error=self.set_fail,
|
|
2112
|
+
)
|
|
2113
|
+
|
|
2114
|
+
# Test that device_id from context is used
|
|
2115
|
+
with new_context():
|
|
2116
|
+
set_context_device_id("context-device-id")
|
|
2117
|
+
client.get_feature_flag("random_key", "some_id")
|
|
2118
|
+
patch_flags.assert_called_with(
|
|
2119
|
+
"random_key",
|
|
2120
|
+
"https://us.i.posthog.com",
|
|
2121
|
+
timeout=3,
|
|
2122
|
+
distinct_id="some_id",
|
|
2123
|
+
groups={},
|
|
2124
|
+
person_properties={"distinct_id": "some_id"},
|
|
2125
|
+
group_properties={},
|
|
2126
|
+
geoip_disable=True,
|
|
2127
|
+
device_id="context-device-id",
|
|
2128
|
+
flag_keys_to_evaluate=["random_key"],
|
|
2129
|
+
)
|
|
2130
|
+
|
|
2131
|
+
# Test that explicit device_id overrides context
|
|
2132
|
+
patch_flags.reset_mock()
|
|
2133
|
+
with new_context():
|
|
2134
|
+
set_context_device_id("context-device-id")
|
|
2135
|
+
client.get_feature_flag(
|
|
2136
|
+
"random_key", "some_id", device_id="explicit-device-id"
|
|
2137
|
+
)
|
|
2138
|
+
patch_flags.assert_called_with(
|
|
2139
|
+
"random_key",
|
|
2140
|
+
"https://us.i.posthog.com",
|
|
2141
|
+
timeout=3,
|
|
2142
|
+
distinct_id="some_id",
|
|
2143
|
+
groups={},
|
|
2144
|
+
person_properties={"distinct_id": "some_id"},
|
|
2145
|
+
group_properties={},
|
|
2146
|
+
geoip_disable=True,
|
|
2147
|
+
device_id="explicit-device-id",
|
|
2148
|
+
flag_keys_to_evaluate=["random_key"],
|
|
2149
|
+
)
|
|
2150
|
+
|
|
2036
2151
|
@parameterized.expand(
|
|
2037
2152
|
[
|
|
2038
2153
|
# name, sys_platform, version_info, expected_runtime, expected_version, expected_os, expected_os_version, platform_method, platform_return, distro_info
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
3
|
import unittest
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
import mock
|
|
7
|
+
from parameterized import parameterized
|
|
6
8
|
|
|
7
9
|
try:
|
|
8
10
|
from queue import Queue
|
|
@@ -14,15 +16,19 @@ from posthoganalytics.request import APIError
|
|
|
14
16
|
from posthoganalytics.test.test_utils import TEST_API_KEY
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
def _track_event(event_name: str = "python event") -> dict[str, str]:
|
|
20
|
+
return {"type": "track", "event": event_name, "distinct_id": "distinct_id"}
|
|
21
|
+
|
|
22
|
+
|
|
17
23
|
class TestConsumer(unittest.TestCase):
|
|
18
|
-
def test_next(self):
|
|
24
|
+
def test_next(self) -> None:
|
|
19
25
|
q = Queue()
|
|
20
26
|
consumer = Consumer(q, "")
|
|
21
27
|
q.put(1)
|
|
22
28
|
next = consumer.next()
|
|
23
29
|
self.assertEqual(next, [1])
|
|
24
30
|
|
|
25
|
-
def test_next_limit(self):
|
|
31
|
+
def test_next_limit(self) -> None:
|
|
26
32
|
q = Queue()
|
|
27
33
|
flush_at = 50
|
|
28
34
|
consumer = Consumer(q, "", flush_at)
|
|
@@ -31,7 +37,7 @@ class TestConsumer(unittest.TestCase):
|
|
|
31
37
|
next = consumer.next()
|
|
32
38
|
self.assertEqual(next, list(range(flush_at)))
|
|
33
39
|
|
|
34
|
-
def test_dropping_oversize_msg(self):
|
|
40
|
+
def test_dropping_oversize_msg(self) -> None:
|
|
35
41
|
q = Queue()
|
|
36
42
|
consumer = Consumer(q, "")
|
|
37
43
|
oversize_msg = {"m": "x" * MAX_MSG_SIZE}
|
|
@@ -40,15 +46,14 @@ class TestConsumer(unittest.TestCase):
|
|
|
40
46
|
self.assertEqual(next, [])
|
|
41
47
|
self.assertTrue(q.empty())
|
|
42
48
|
|
|
43
|
-
def test_upload(self):
|
|
49
|
+
def test_upload(self) -> None:
|
|
44
50
|
q = Queue()
|
|
45
51
|
consumer = Consumer(q, TEST_API_KEY)
|
|
46
|
-
|
|
47
|
-
q.put(track)
|
|
52
|
+
q.put(_track_event())
|
|
48
53
|
success = consumer.upload()
|
|
49
54
|
self.assertTrue(success)
|
|
50
55
|
|
|
51
|
-
def test_flush_interval(self):
|
|
56
|
+
def test_flush_interval(self) -> None:
|
|
52
57
|
# Put _n_ items in the queue, pausing a little bit more than
|
|
53
58
|
# _flush_interval_ after each one.
|
|
54
59
|
# The consumer should upload _n_ times.
|
|
@@ -57,17 +62,12 @@ class TestConsumer(unittest.TestCase):
|
|
|
57
62
|
consumer = Consumer(q, TEST_API_KEY, flush_at=10, flush_interval=flush_interval)
|
|
58
63
|
with mock.patch("posthog.consumer.batch_post") as mock_post:
|
|
59
64
|
consumer.start()
|
|
60
|
-
for i in range(
|
|
61
|
-
|
|
62
|
-
"type": "track",
|
|
63
|
-
"event": "python event %d" % i,
|
|
64
|
-
"distinct_id": "distinct_id",
|
|
65
|
-
}
|
|
66
|
-
q.put(track)
|
|
65
|
+
for i in range(3):
|
|
66
|
+
q.put(_track_event("python event %d" % i))
|
|
67
67
|
time.sleep(flush_interval * 1.1)
|
|
68
68
|
self.assertEqual(mock_post.call_count, 3)
|
|
69
69
|
|
|
70
|
-
def test_multiple_uploads_per_interval(self):
|
|
70
|
+
def test_multiple_uploads_per_interval(self) -> None:
|
|
71
71
|
# Put _flush_at*2_ items in the queue at once, then pause for
|
|
72
72
|
# _flush_interval_. The consumer should upload 2 times.
|
|
73
73
|
q = Queue()
|
|
@@ -78,88 +78,60 @@ class TestConsumer(unittest.TestCase):
|
|
|
78
78
|
)
|
|
79
79
|
with mock.patch("posthog.consumer.batch_post") as mock_post:
|
|
80
80
|
consumer.start()
|
|
81
|
-
for i in range(
|
|
82
|
-
|
|
83
|
-
"type": "track",
|
|
84
|
-
"event": "python event %d" % i,
|
|
85
|
-
"distinct_id": "distinct_id",
|
|
86
|
-
}
|
|
87
|
-
q.put(track)
|
|
81
|
+
for i in range(flush_at * 2):
|
|
82
|
+
q.put(_track_event("python event %d" % i))
|
|
88
83
|
time.sleep(flush_interval * 1.1)
|
|
89
84
|
self.assertEqual(mock_post.call_count, 2)
|
|
90
85
|
|
|
91
|
-
def test_request(self):
|
|
86
|
+
def test_request(self) -> None:
|
|
92
87
|
consumer = Consumer(None, TEST_API_KEY)
|
|
93
|
-
|
|
94
|
-
consumer.request([track])
|
|
88
|
+
consumer.request([_track_event()])
|
|
95
89
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
raise expected_exception
|
|
90
|
+
def _run_retry_test(
|
|
91
|
+
self, exception: Exception, exception_count: int, retries: int = 10
|
|
92
|
+
) -> None:
|
|
93
|
+
call_count = [0]
|
|
101
94
|
|
|
102
|
-
mock_post
|
|
95
|
+
def mock_post(*args: Any, **kwargs: Any) -> None:
|
|
96
|
+
call_count[0] += 1
|
|
97
|
+
if call_count[0] <= exception_count:
|
|
98
|
+
raise exception
|
|
103
99
|
|
|
100
|
+
consumer = Consumer(None, TEST_API_KEY, retries=retries)
|
|
104
101
|
with mock.patch(
|
|
105
102
|
"posthog.consumer.batch_post", mock.Mock(side_effect=mock_post)
|
|
106
103
|
):
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"event": "python event",
|
|
110
|
-
"distinct_id": "distinct_id",
|
|
111
|
-
}
|
|
112
|
-
# request() should succeed if the number of exceptions raised is
|
|
113
|
-
# less than the retries paramater.
|
|
114
|
-
if exception_count <= consumer.retries:
|
|
115
|
-
consumer.request([track])
|
|
104
|
+
if exception_count <= retries:
|
|
105
|
+
consumer.request([_track_event()])
|
|
116
106
|
else:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
self.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
consumer = Consumer(None, TEST_API_KEY)
|
|
141
|
-
self._test_request_retry(consumer, APIError(429, "Too Many Requests"), 2)
|
|
142
|
-
|
|
143
|
-
# we should NOT retry on other client errors
|
|
144
|
-
consumer = Consumer(None, TEST_API_KEY)
|
|
145
|
-
api_error = APIError(400, "Client Errors")
|
|
146
|
-
try:
|
|
147
|
-
self._test_request_retry(consumer, api_error, 1)
|
|
148
|
-
except APIError:
|
|
149
|
-
pass
|
|
150
|
-
else:
|
|
151
|
-
self.fail("request() should not retry on client errors")
|
|
152
|
-
|
|
153
|
-
# test for number of exceptions raise > retries value
|
|
154
|
-
consumer = Consumer(None, TEST_API_KEY, retries=3)
|
|
155
|
-
self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 3)
|
|
156
|
-
|
|
157
|
-
def test_pause(self):
|
|
107
|
+
with self.assertRaises(type(exception)):
|
|
108
|
+
consumer.request([_track_event()])
|
|
109
|
+
|
|
110
|
+
@parameterized.expand(
|
|
111
|
+
[
|
|
112
|
+
("general_errors", Exception("generic exception"), 2),
|
|
113
|
+
("server_errors", APIError(500, "Internal Server Error"), 2),
|
|
114
|
+
("rate_limit_errors", APIError(429, "Too Many Requests"), 2),
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
def test_request_retries_on_retriable_errors(
|
|
118
|
+
self, _name: str, exception: Exception, exception_count: int
|
|
119
|
+
) -> None:
|
|
120
|
+
self._run_retry_test(exception, exception_count)
|
|
121
|
+
|
|
122
|
+
def test_request_does_not_retry_client_errors(self) -> None:
|
|
123
|
+
with self.assertRaises(APIError):
|
|
124
|
+
self._run_retry_test(APIError(400, "Client Errors"), 1)
|
|
125
|
+
|
|
126
|
+
def test_request_fails_when_exceptions_exceed_retries(self) -> None:
|
|
127
|
+
self._run_retry_test(APIError(500, "Internal Server Error"), 4, retries=3)
|
|
128
|
+
|
|
129
|
+
def test_pause(self) -> None:
|
|
158
130
|
consumer = Consumer(None, TEST_API_KEY)
|
|
159
131
|
consumer.pause()
|
|
160
132
|
self.assertFalse(consumer.running)
|
|
161
133
|
|
|
162
|
-
def test_max_batch_size(self):
|
|
134
|
+
def test_max_batch_size(self) -> None:
|
|
163
135
|
q = Queue()
|
|
164
136
|
consumer = Consumer(q, TEST_API_KEY, flush_at=100000, flush_interval=3)
|
|
165
137
|
properties = {}
|
|
@@ -175,7 +147,7 @@ class TestConsumer(unittest.TestCase):
|
|
|
175
147
|
# Let's capture 8MB of data to trigger two batches
|
|
176
148
|
n_msgs = int(8_000_000 / msg_size)
|
|
177
149
|
|
|
178
|
-
def mock_post_fn(_, data, **kwargs):
|
|
150
|
+
def mock_post_fn(_: str, data: str, **kwargs: Any) -> mock.Mock:
|
|
179
151
|
res = mock.Mock()
|
|
180
152
|
res.status_code = 200
|
|
181
153
|
request_size = len(data.encode())
|
|
@@ -194,3 +166,34 @@ class TestConsumer(unittest.TestCase):
|
|
|
194
166
|
q.put(track)
|
|
195
167
|
q.join()
|
|
196
168
|
self.assertEqual(mock_post.call_count, 2)
|
|
169
|
+
|
|
170
|
+
@parameterized.expand(
|
|
171
|
+
[
|
|
172
|
+
("on_error_succeeds", False),
|
|
173
|
+
("on_error_raises", True),
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
def test_upload_exception_calls_on_error_and_does_not_raise(
|
|
177
|
+
self, _name: str, on_error_raises: bool
|
|
178
|
+
) -> None:
|
|
179
|
+
on_error_called: list[tuple[Exception, list[dict[str, str]]]] = []
|
|
180
|
+
|
|
181
|
+
def on_error(e: Exception, batch: list[dict[str, str]]) -> None:
|
|
182
|
+
on_error_called.append((e, batch))
|
|
183
|
+
if on_error_raises:
|
|
184
|
+
raise Exception("on_error failed")
|
|
185
|
+
|
|
186
|
+
q = Queue()
|
|
187
|
+
consumer = Consumer(q, TEST_API_KEY, on_error=on_error)
|
|
188
|
+
track = _track_event()
|
|
189
|
+
q.put(track)
|
|
190
|
+
|
|
191
|
+
with mock.patch.object(
|
|
192
|
+
consumer, "request", side_effect=Exception("request failed")
|
|
193
|
+
):
|
|
194
|
+
result = consumer.upload()
|
|
195
|
+
|
|
196
|
+
self.assertFalse(result)
|
|
197
|
+
self.assertEqual(len(on_error_called), 1)
|
|
198
|
+
self.assertEqual(str(on_error_called[0][0]), "request failed")
|
|
199
|
+
self.assertEqual(on_error_called[0][1], [track])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: posthoganalytics
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.6.0
|
|
4
4
|
Summary: Integrate PostHog into any python application.
|
|
5
5
|
Home-page: https://github.com/posthog/posthog-python
|
|
6
6
|
Author: Posthog
|
|
@@ -87,6 +87,14 @@ Dynamic: maintainer
|
|
|
87
87
|
|
|
88
88
|
Please see the [Python integration docs](https://posthog.com/docs/integrations/python-integration) for details.
|
|
89
89
|
|
|
90
|
+
## Python Version Support
|
|
91
|
+
|
|
92
|
+
| SDK Version | Python Versions Supported | Notes |
|
|
93
|
+
|-------------|---------------------------|-------|
|
|
94
|
+
| 7.3.1+ | 3.10, 3.11, 3.12, 3.13, 3.14 | Added Python 3.14 support |
|
|
95
|
+
| 7.0.0 - 7.0.1 | 3.10, 3.11, 3.12, 3.13 | Dropped Python 3.9 support |
|
|
96
|
+
| 4.0.1 - 6.x | 3.9, 3.10, 3.11, 3.12, 3.13 | Python 3.9+ required |
|
|
97
|
+
|
|
90
98
|
## Development
|
|
91
99
|
|
|
92
100
|
### Testing Locally
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/anthropic/anthropic.py
RENAMED
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/anthropic/anthropic_async.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/gemini/gemini_async.py
RENAMED
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/gemini/gemini_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/langchain/callbacks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/openai/openai_async.py
RENAMED
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/ai/openai/openai_converter.py
RENAMED
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.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
|
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_exception_capture.py
RENAMED
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_feature_flag.py
RENAMED
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_feature_flag_result.py
RENAMED
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_feature_flags.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.0}/posthoganalytics/test/test_size_limited_dict.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-7.5.0 → posthoganalytics-7.6.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
|