posthog 6.7.2__py3-none-any.whl → 6.9.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.
- posthog/__init__.py +30 -2
- posthog/ai/anthropic/anthropic.py +4 -5
- posthog/ai/anthropic/anthropic_async.py +33 -70
- posthog/ai/anthropic/anthropic_converter.py +73 -23
- posthog/ai/gemini/gemini.py +11 -10
- posthog/ai/gemini/gemini_converter.py +177 -29
- posthog/ai/langchain/callbacks.py +18 -3
- posthog/ai/openai/openai.py +8 -8
- posthog/ai/openai/openai_async.py +36 -15
- posthog/ai/openai/openai_converter.py +192 -42
- posthog/ai/types.py +2 -19
- posthog/ai/utils.py +124 -118
- posthog/client.py +96 -4
- posthog/contexts.py +81 -0
- posthog/exception_utils.py +192 -0
- posthog/feature_flags.py +26 -10
- posthog/integrations/django.py +157 -19
- posthog/test/test_client.py +43 -0
- posthog/test/test_exception_capture.py +300 -0
- posthog/test/test_feature_flags.py +146 -35
- posthog/test/test_module.py +0 -8
- posthog/version.py +1 -1
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/METADATA +1 -1
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/RECORD +27 -27
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/WHEEL +0 -0
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/licenses/LICENSE +0 -0
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/top_level.txt +0 -0
|
@@ -32,3 +32,303 @@ def test_excepthook(tmpdir):
|
|
|
32
32
|
b'"$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"platform": "python", "filename": "app.py", "abs_path"'
|
|
33
33
|
in output
|
|
34
34
|
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_code_variables_capture(tmpdir):
|
|
38
|
+
app = tmpdir.join("app.py")
|
|
39
|
+
app.write(
|
|
40
|
+
dedent(
|
|
41
|
+
"""
|
|
42
|
+
import os
|
|
43
|
+
from posthog import Posthog
|
|
44
|
+
|
|
45
|
+
class UnserializableObject:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
posthog = Posthog(
|
|
49
|
+
'phc_x',
|
|
50
|
+
host='https://eu.i.posthog.com',
|
|
51
|
+
debug=True,
|
|
52
|
+
enable_exception_autocapture=True,
|
|
53
|
+
capture_exception_code_variables=True,
|
|
54
|
+
project_root=os.path.dirname(os.path.abspath(__file__))
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def trigger_error():
|
|
58
|
+
my_string = "hello world"
|
|
59
|
+
my_number = 42
|
|
60
|
+
my_bool = True
|
|
61
|
+
my_dict = {"name": "test", "value": 123}
|
|
62
|
+
my_obj = UnserializableObject()
|
|
63
|
+
my_password = "secret123" # Should be masked by default
|
|
64
|
+
__should_be_ignored = "hidden" # Should be ignored by default
|
|
65
|
+
|
|
66
|
+
1/0 # Trigger exception
|
|
67
|
+
|
|
68
|
+
def intermediate_function():
|
|
69
|
+
request_id = "abc-123"
|
|
70
|
+
user_count = 100
|
|
71
|
+
is_active = True
|
|
72
|
+
|
|
73
|
+
trigger_error()
|
|
74
|
+
|
|
75
|
+
def process_data():
|
|
76
|
+
batch_size = 50
|
|
77
|
+
retry_count = 3
|
|
78
|
+
|
|
79
|
+
intermediate_function()
|
|
80
|
+
|
|
81
|
+
process_data()
|
|
82
|
+
"""
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
with pytest.raises(subprocess.CalledProcessError) as excinfo:
|
|
87
|
+
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
|
|
88
|
+
|
|
89
|
+
output = excinfo.value.output
|
|
90
|
+
|
|
91
|
+
assert b"ZeroDivisionError" in output
|
|
92
|
+
assert b"code_variables" in output
|
|
93
|
+
|
|
94
|
+
# Variables from trigger_error frame
|
|
95
|
+
assert b"'my_string': 'hello world'" in output
|
|
96
|
+
assert b"'my_number': 42" in output
|
|
97
|
+
assert b"'my_bool': 'True'" in output
|
|
98
|
+
assert b'"my_dict": "{\\"name\\": \\"test\\", \\"value\\": 123}"' in output
|
|
99
|
+
assert b'"my_obj": "<UnserializableObject>"' in output
|
|
100
|
+
assert b"'my_password': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
|
|
101
|
+
assert b"'__should_be_ignored':" not in output
|
|
102
|
+
|
|
103
|
+
# Variables from intermediate_function frame
|
|
104
|
+
assert b"'request_id': 'abc-123'" in output
|
|
105
|
+
assert b"'user_count': 100" in output
|
|
106
|
+
assert b"'is_active': 'True'" in output
|
|
107
|
+
|
|
108
|
+
# Variables from process_data frame
|
|
109
|
+
assert b"'batch_size': 50" in output
|
|
110
|
+
assert b"'retry_count': 3" in output
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_code_variables_context_override(tmpdir):
|
|
114
|
+
app = tmpdir.join("app.py")
|
|
115
|
+
app.write(
|
|
116
|
+
dedent(
|
|
117
|
+
"""
|
|
118
|
+
import os
|
|
119
|
+
import posthog
|
|
120
|
+
from posthog import Posthog
|
|
121
|
+
|
|
122
|
+
posthog_client = Posthog(
|
|
123
|
+
'phc_x',
|
|
124
|
+
host='https://eu.i.posthog.com',
|
|
125
|
+
debug=True,
|
|
126
|
+
enable_exception_autocapture=True,
|
|
127
|
+
capture_exception_code_variables=False,
|
|
128
|
+
project_root=os.path.dirname(os.path.abspath(__file__))
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def process_data():
|
|
132
|
+
bank = "should_be_masked"
|
|
133
|
+
__dunder_var = "should_be_visible"
|
|
134
|
+
|
|
135
|
+
1/0
|
|
136
|
+
|
|
137
|
+
with posthog.new_context(client=posthog_client):
|
|
138
|
+
posthog.set_capture_exception_code_variables_context(True)
|
|
139
|
+
posthog.set_code_variables_mask_patterns_context([r"(?i).*bank.*"])
|
|
140
|
+
posthog.set_code_variables_ignore_patterns_context([])
|
|
141
|
+
|
|
142
|
+
process_data()
|
|
143
|
+
"""
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
with pytest.raises(subprocess.CalledProcessError) as excinfo:
|
|
148
|
+
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
|
|
149
|
+
|
|
150
|
+
output = excinfo.value.output
|
|
151
|
+
|
|
152
|
+
assert b"ZeroDivisionError" in output
|
|
153
|
+
assert b"code_variables" in output
|
|
154
|
+
assert b"'bank': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
|
|
155
|
+
assert b"'__dunder_var': 'should_be_visible'" in output
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_code_variables_size_limiter(tmpdir):
|
|
159
|
+
app = tmpdir.join("app.py")
|
|
160
|
+
app.write(
|
|
161
|
+
dedent(
|
|
162
|
+
"""
|
|
163
|
+
import os
|
|
164
|
+
from posthog import Posthog
|
|
165
|
+
|
|
166
|
+
posthog = Posthog(
|
|
167
|
+
'phc_x',
|
|
168
|
+
host='https://eu.i.posthog.com',
|
|
169
|
+
debug=True,
|
|
170
|
+
enable_exception_autocapture=True,
|
|
171
|
+
capture_exception_code_variables=True,
|
|
172
|
+
project_root=os.path.dirname(os.path.abspath(__file__))
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def trigger_error():
|
|
176
|
+
var_a = "a" * 2000
|
|
177
|
+
var_b = "b" * 2000
|
|
178
|
+
var_c = "c" * 2000
|
|
179
|
+
var_d = "d" * 2000
|
|
180
|
+
var_e = "e" * 2000
|
|
181
|
+
var_f = "f" * 2000
|
|
182
|
+
var_g = "g" * 2000
|
|
183
|
+
|
|
184
|
+
1/0
|
|
185
|
+
|
|
186
|
+
def intermediate_function():
|
|
187
|
+
var_h = "h" * 2000
|
|
188
|
+
var_i = "i" * 2000
|
|
189
|
+
var_j = "j" * 2000
|
|
190
|
+
var_k = "k" * 2000
|
|
191
|
+
var_l = "l" * 2000
|
|
192
|
+
var_m = "m" * 2000
|
|
193
|
+
var_n = "n" * 2000
|
|
194
|
+
|
|
195
|
+
trigger_error()
|
|
196
|
+
|
|
197
|
+
def process_data():
|
|
198
|
+
var_o = "o" * 2000
|
|
199
|
+
var_p = "p" * 2000
|
|
200
|
+
var_q = "q" * 2000
|
|
201
|
+
var_r = "r" * 2000
|
|
202
|
+
var_s = "s" * 2000
|
|
203
|
+
var_t = "t" * 2000
|
|
204
|
+
var_u = "u" * 2000
|
|
205
|
+
|
|
206
|
+
intermediate_function()
|
|
207
|
+
|
|
208
|
+
process_data()
|
|
209
|
+
"""
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
with pytest.raises(subprocess.CalledProcessError) as excinfo:
|
|
214
|
+
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
|
|
215
|
+
|
|
216
|
+
output = excinfo.value.output.decode("utf-8")
|
|
217
|
+
|
|
218
|
+
assert "ZeroDivisionError" in output
|
|
219
|
+
assert "code_variables" in output
|
|
220
|
+
|
|
221
|
+
captured_vars = []
|
|
222
|
+
for var_name in [
|
|
223
|
+
"var_a",
|
|
224
|
+
"var_b",
|
|
225
|
+
"var_c",
|
|
226
|
+
"var_d",
|
|
227
|
+
"var_e",
|
|
228
|
+
"var_f",
|
|
229
|
+
"var_g",
|
|
230
|
+
"var_h",
|
|
231
|
+
"var_i",
|
|
232
|
+
"var_j",
|
|
233
|
+
"var_k",
|
|
234
|
+
"var_l",
|
|
235
|
+
"var_m",
|
|
236
|
+
"var_n",
|
|
237
|
+
"var_o",
|
|
238
|
+
"var_p",
|
|
239
|
+
"var_q",
|
|
240
|
+
"var_r",
|
|
241
|
+
"var_s",
|
|
242
|
+
"var_t",
|
|
243
|
+
"var_u",
|
|
244
|
+
]:
|
|
245
|
+
if f"'{var_name}'" in output:
|
|
246
|
+
captured_vars.append(var_name)
|
|
247
|
+
|
|
248
|
+
assert len(captured_vars) > 0
|
|
249
|
+
assert len(captured_vars) < 21
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_code_variables_disabled_capture(tmpdir):
|
|
253
|
+
app = tmpdir.join("app.py")
|
|
254
|
+
app.write(
|
|
255
|
+
dedent(
|
|
256
|
+
"""
|
|
257
|
+
import os
|
|
258
|
+
from posthog import Posthog
|
|
259
|
+
|
|
260
|
+
posthog = Posthog(
|
|
261
|
+
'phc_x',
|
|
262
|
+
host='https://eu.i.posthog.com',
|
|
263
|
+
debug=True,
|
|
264
|
+
enable_exception_autocapture=True,
|
|
265
|
+
capture_exception_code_variables=False,
|
|
266
|
+
project_root=os.path.dirname(os.path.abspath(__file__))
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def trigger_error():
|
|
270
|
+
my_string = "hello world"
|
|
271
|
+
my_number = 42
|
|
272
|
+
my_bool = True
|
|
273
|
+
|
|
274
|
+
1/0
|
|
275
|
+
|
|
276
|
+
trigger_error()
|
|
277
|
+
"""
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
with pytest.raises(subprocess.CalledProcessError) as excinfo:
|
|
282
|
+
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
|
|
283
|
+
|
|
284
|
+
output = excinfo.value.output.decode("utf-8")
|
|
285
|
+
|
|
286
|
+
assert "ZeroDivisionError" in output
|
|
287
|
+
assert "'code_variables':" not in output
|
|
288
|
+
assert '"code_variables":' not in output
|
|
289
|
+
assert "'my_string'" not in output
|
|
290
|
+
assert "'my_number'" not in output
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_code_variables_enabled_then_disabled_in_context(tmpdir):
|
|
294
|
+
app = tmpdir.join("app.py")
|
|
295
|
+
app.write(
|
|
296
|
+
dedent(
|
|
297
|
+
"""
|
|
298
|
+
import os
|
|
299
|
+
import posthog
|
|
300
|
+
from posthog import Posthog
|
|
301
|
+
|
|
302
|
+
posthog_client = Posthog(
|
|
303
|
+
'phc_x',
|
|
304
|
+
host='https://eu.i.posthog.com',
|
|
305
|
+
debug=True,
|
|
306
|
+
enable_exception_autocapture=True,
|
|
307
|
+
capture_exception_code_variables=True,
|
|
308
|
+
project_root=os.path.dirname(os.path.abspath(__file__))
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def process_data():
|
|
312
|
+
my_var = "should not be captured"
|
|
313
|
+
important_value = 123
|
|
314
|
+
|
|
315
|
+
1/0
|
|
316
|
+
|
|
317
|
+
with posthog.new_context(client=posthog_client):
|
|
318
|
+
posthog.set_capture_exception_code_variables_context(False)
|
|
319
|
+
|
|
320
|
+
process_data()
|
|
321
|
+
"""
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
with pytest.raises(subprocess.CalledProcessError) as excinfo:
|
|
326
|
+
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
|
|
327
|
+
|
|
328
|
+
output = excinfo.value.output.decode("utf-8")
|
|
329
|
+
|
|
330
|
+
assert "ZeroDivisionError" in output
|
|
331
|
+
assert "'code_variables':" not in output
|
|
332
|
+
assert '"code_variables":' not in output
|
|
333
|
+
assert "'my_var'" not in output
|
|
334
|
+
assert "'important_value'" not in output
|
|
@@ -2804,73 +2804,61 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
2804
2804
|
self.assertEqual(patch_flags.call_count, 0)
|
|
2805
2805
|
|
|
2806
2806
|
@mock.patch("posthog.client.flags")
|
|
2807
|
-
def
|
|
2808
|
-
patch_flags.return_value = {"featureFlags": {"
|
|
2807
|
+
def test_conditions_evaluated_in_order(self, patch_flags):
|
|
2808
|
+
patch_flags.return_value = {"featureFlags": {"order-test": "server-variant"}}
|
|
2809
2809
|
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
|
2810
2810
|
client.feature_flags = [
|
|
2811
2811
|
{
|
|
2812
2812
|
"id": 1,
|
|
2813
|
-
"name": "
|
|
2814
|
-
"key": "
|
|
2813
|
+
"name": "Order Test Flag",
|
|
2814
|
+
"key": "order-test",
|
|
2815
2815
|
"active": True,
|
|
2816
|
-
"rollout_percentage": 100,
|
|
2817
2816
|
"filters": {
|
|
2818
2817
|
"groups": [
|
|
2819
2818
|
{
|
|
2820
2819
|
"rollout_percentage": 100,
|
|
2821
|
-
# The override applies even if the first condition matches all and gives everyone their default group
|
|
2822
2820
|
},
|
|
2823
2821
|
{
|
|
2824
2822
|
"properties": [
|
|
2825
2823
|
{
|
|
2826
2824
|
"key": "email",
|
|
2827
2825
|
"type": "person",
|
|
2828
|
-
"value": "
|
|
2829
|
-
"operator": "
|
|
2826
|
+
"value": "@vip.com",
|
|
2827
|
+
"operator": "icontains",
|
|
2830
2828
|
}
|
|
2831
2829
|
],
|
|
2832
2830
|
"rollout_percentage": 100,
|
|
2833
|
-
"variant": "
|
|
2831
|
+
"variant": "vip-variant",
|
|
2834
2832
|
},
|
|
2835
|
-
{"rollout_percentage": 50, "variant": "third-variant"},
|
|
2836
2833
|
],
|
|
2837
2834
|
"multivariate": {
|
|
2838
2835
|
"variants": [
|
|
2839
2836
|
{
|
|
2840
|
-
"key": "
|
|
2841
|
-
"name": "
|
|
2842
|
-
"rollout_percentage":
|
|
2843
|
-
},
|
|
2844
|
-
{
|
|
2845
|
-
"key": "second-variant",
|
|
2846
|
-
"name": "Second Variant",
|
|
2847
|
-
"rollout_percentage": 25,
|
|
2837
|
+
"key": "control",
|
|
2838
|
+
"name": "Control",
|
|
2839
|
+
"rollout_percentage": 100,
|
|
2848
2840
|
},
|
|
2849
2841
|
{
|
|
2850
|
-
"key": "
|
|
2851
|
-
"name": "
|
|
2852
|
-
"rollout_percentage":
|
|
2842
|
+
"key": "vip-variant",
|
|
2843
|
+
"name": "VIP Variant",
|
|
2844
|
+
"rollout_percentage": 0,
|
|
2853
2845
|
},
|
|
2854
2846
|
]
|
|
2855
2847
|
},
|
|
2856
2848
|
},
|
|
2857
2849
|
}
|
|
2858
2850
|
]
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
"
|
|
2866
|
-
)
|
|
2867
|
-
self.assertEqual(
|
|
2868
|
-
client.get_feature_flag("beta-feature", "example_id"), "third-variant"
|
|
2869
|
-
)
|
|
2870
|
-
self.assertEqual(
|
|
2871
|
-
client.get_feature_flag("beta-feature", "another_id"), "second-variant"
|
|
2851
|
+
|
|
2852
|
+
# Even though user@vip.com would match the second condition with variant override,
|
|
2853
|
+
# they should match the first condition and get control
|
|
2854
|
+
result = client.get_feature_flag(
|
|
2855
|
+
"order-test",
|
|
2856
|
+
"user123",
|
|
2857
|
+
person_properties={"email": "user@vip.com"},
|
|
2872
2858
|
)
|
|
2873
|
-
|
|
2859
|
+
self.assertEqual(result, "control")
|
|
2860
|
+
|
|
2861
|
+
# server not called because this can be evaluated locally
|
|
2874
2862
|
self.assertEqual(patch_flags.call_count, 0)
|
|
2875
2863
|
|
|
2876
2864
|
@mock.patch("posthog.client.flags")
|
|
@@ -3025,6 +3013,75 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
3025
3013
|
)
|
|
3026
3014
|
self.assertEqual(patch_flags.call_count, 0)
|
|
3027
3015
|
|
|
3016
|
+
@mock.patch("posthog.client.flags")
|
|
3017
|
+
@mock.patch("posthog.client.get")
|
|
3018
|
+
def test_fallback_to_api_when_flag_has_static_cohort_in_multi_condition(
|
|
3019
|
+
self, patch_get, patch_flags
|
|
3020
|
+
):
|
|
3021
|
+
"""
|
|
3022
|
+
When a flag has multiple conditions and one contains a static cohort,
|
|
3023
|
+
the SDK should fallback to API for the entire flag, not just skip that
|
|
3024
|
+
condition and evaluate the next one locally.
|
|
3025
|
+
|
|
3026
|
+
This prevents returning wrong variants when later conditions could match
|
|
3027
|
+
locally but the user is actually in the static cohort.
|
|
3028
|
+
"""
|
|
3029
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
3030
|
+
|
|
3031
|
+
# Mock the local flags response - cohort 999 is NOT in cohorts map (static cohort)
|
|
3032
|
+
client.feature_flags = [
|
|
3033
|
+
{
|
|
3034
|
+
"id": 1,
|
|
3035
|
+
"key": "multi-condition-flag",
|
|
3036
|
+
"active": True,
|
|
3037
|
+
"filters": {
|
|
3038
|
+
"groups": [
|
|
3039
|
+
{
|
|
3040
|
+
"properties": [
|
|
3041
|
+
{"key": "id", "value": 999, "type": "cohort"}
|
|
3042
|
+
],
|
|
3043
|
+
"rollout_percentage": 100,
|
|
3044
|
+
"variant": "set-1",
|
|
3045
|
+
},
|
|
3046
|
+
{
|
|
3047
|
+
"properties": [
|
|
3048
|
+
{
|
|
3049
|
+
"key": "$geoip_country_code",
|
|
3050
|
+
"operator": "exact",
|
|
3051
|
+
"value": ["DE"],
|
|
3052
|
+
"type": "person",
|
|
3053
|
+
}
|
|
3054
|
+
],
|
|
3055
|
+
"rollout_percentage": 100,
|
|
3056
|
+
"variant": "set-8",
|
|
3057
|
+
},
|
|
3058
|
+
],
|
|
3059
|
+
"multivariate": {
|
|
3060
|
+
"variants": [
|
|
3061
|
+
{"key": "set-1", "rollout_percentage": 50},
|
|
3062
|
+
{"key": "set-8", "rollout_percentage": 50},
|
|
3063
|
+
]
|
|
3064
|
+
},
|
|
3065
|
+
},
|
|
3066
|
+
}
|
|
3067
|
+
]
|
|
3068
|
+
client.cohorts = {} # Note: cohort 999 is NOT here - it's a static cohort
|
|
3069
|
+
|
|
3070
|
+
# Mock the API response - user is in the static cohort
|
|
3071
|
+
patch_flags.return_value = {"featureFlags": {"multi-condition-flag": "set-1"}}
|
|
3072
|
+
|
|
3073
|
+
result = client.get_feature_flag(
|
|
3074
|
+
"multi-condition-flag",
|
|
3075
|
+
"test-distinct-id",
|
|
3076
|
+
person_properties={"$geoip_country_code": "DE"},
|
|
3077
|
+
)
|
|
3078
|
+
|
|
3079
|
+
# Should return the API result (set-1), not local evaluation (set-8)
|
|
3080
|
+
self.assertEqual(result, "set-1")
|
|
3081
|
+
|
|
3082
|
+
# Verify API was called (fallback occurred)
|
|
3083
|
+
self.assertEqual(patch_flags.call_count, 1)
|
|
3084
|
+
|
|
3028
3085
|
|
|
3029
3086
|
class TestMatchProperties(unittest.TestCase):
|
|
3030
3087
|
def property(self, key, value, operator=None):
|
|
@@ -4018,6 +4075,60 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
4018
4075
|
|
|
4019
4076
|
patch_capture.reset_mock()
|
|
4020
4077
|
|
|
4078
|
+
@mock.patch("posthog.client.flags")
|
|
4079
|
+
def test_fallback_to_api_in_get_feature_flag_payload_when_flag_has_static_cohort(
|
|
4080
|
+
self, patch_flags
|
|
4081
|
+
):
|
|
4082
|
+
"""
|
|
4083
|
+
Test that get_feature_flag_payload falls back to API when evaluating
|
|
4084
|
+
a flag with static cohorts, similar to get_feature_flag behavior.
|
|
4085
|
+
"""
|
|
4086
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
4087
|
+
|
|
4088
|
+
# Mock the local flags response - cohort 999 is NOT in cohorts map (static cohort)
|
|
4089
|
+
client.feature_flags = [
|
|
4090
|
+
{
|
|
4091
|
+
"id": 1,
|
|
4092
|
+
"name": "Multi-condition Flag",
|
|
4093
|
+
"key": "multi-condition-flag",
|
|
4094
|
+
"active": True,
|
|
4095
|
+
"filters": {
|
|
4096
|
+
"groups": [
|
|
4097
|
+
{
|
|
4098
|
+
"properties": [
|
|
4099
|
+
{"key": "id", "value": 999, "type": "cohort"}
|
|
4100
|
+
],
|
|
4101
|
+
"rollout_percentage": 100,
|
|
4102
|
+
"variant": "variant-1",
|
|
4103
|
+
}
|
|
4104
|
+
],
|
|
4105
|
+
"multivariate": {
|
|
4106
|
+
"variants": [{"key": "variant-1", "rollout_percentage": 100}]
|
|
4107
|
+
},
|
|
4108
|
+
"payloads": {"variant-1": '{"message": "local-payload"}'},
|
|
4109
|
+
},
|
|
4110
|
+
}
|
|
4111
|
+
]
|
|
4112
|
+
client.cohorts = {} # Note: cohort 999 is NOT here - it's a static cohort
|
|
4113
|
+
|
|
4114
|
+
# Mock the API response - user is in the static cohort
|
|
4115
|
+
patch_flags.return_value = {
|
|
4116
|
+
"featureFlags": {"multi-condition-flag": "variant-1"},
|
|
4117
|
+
"featureFlagPayloads": {"multi-condition-flag": '{"message": "from-api"}'},
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
# Call get_feature_flag_payload without match_value to trigger evaluation
|
|
4121
|
+
result = client.get_feature_flag_payload(
|
|
4122
|
+
"multi-condition-flag",
|
|
4123
|
+
"test-distinct-id",
|
|
4124
|
+
)
|
|
4125
|
+
|
|
4126
|
+
# Should return the API payload, not local payload
|
|
4127
|
+
self.assertEqual(result, {"message": "from-api"})
|
|
4128
|
+
|
|
4129
|
+
# Verify API was called (fallback occurred)
|
|
4130
|
+
self.assertEqual(patch_flags.call_count, 1)
|
|
4131
|
+
|
|
4021
4132
|
@mock.patch.object(Client, "capture")
|
|
4022
4133
|
@mock.patch("posthog.client.flags")
|
|
4023
4134
|
def test_disable_geoip_get_flag_capture_call(self, patch_flags, patch_capture):
|
posthog/test/test_module.py
CHANGED
|
@@ -18,14 +18,6 @@ class TestModule(unittest.TestCase):
|
|
|
18
18
|
"testsecret", host="http://localhost:8000", on_error=self.failed
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
-
def test_no_api_key(self):
|
|
22
|
-
self.posthog.api_key = None
|
|
23
|
-
self.assertRaises(Exception, self.posthog.capture)
|
|
24
|
-
|
|
25
|
-
def test_no_host(self):
|
|
26
|
-
self.posthog.host = None
|
|
27
|
-
self.assertRaises(Exception, self.posthog.capture)
|
|
28
|
-
|
|
29
21
|
def test_track(self):
|
|
30
22
|
res = self.posthog.capture("python module event", distinct_id="distinct_id")
|
|
31
23
|
self._assert_enqueue_result(res)
|
posthog/version.py
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
posthog/__init__.py,sha256=
|
|
1
|
+
posthog/__init__.py,sha256=v2NGzYdoz4GRqCMDxStesELHz3N1j9qhJcNx2FXFsq0,27022
|
|
2
2
|
posthog/args.py,sha256=JUt0vbtF33IzLt3ARgsxMEYYnZo3RNS_LcK4-CjWaco,3298
|
|
3
|
-
posthog/client.py,sha256=
|
|
3
|
+
posthog/client.py,sha256=78sSBQiOszRwVfFVSJ46hpxJ5-mQmRoYUCLkqzz7Bmg,74535
|
|
4
4
|
posthog/consumer.py,sha256=fdteMZ-deJGMpaQmHyznw_cwQG2Vvld1tmN9LUkZPrY,4608
|
|
5
|
-
posthog/contexts.py,sha256=
|
|
5
|
+
posthog/contexts.py,sha256=22z4KySFCTwPbz4OYsd_8EJpoc2H91NiLq9cscSvFfw,12600
|
|
6
6
|
posthog/exception_capture.py,sha256=pmKtjQ6QY6zs4u_-ZA4H1gCyR3iI4sfqCQG_jwe_bKo,1774
|
|
7
|
-
posthog/exception_utils.py,sha256=
|
|
8
|
-
posthog/feature_flags.py,sha256=
|
|
7
|
+
posthog/exception_utils.py,sha256=nn2ue5Xmj1Rv5a6WWK1toScjv4Y2PxPRbVD41tbfKXA,31848
|
|
8
|
+
posthog/feature_flags.py,sha256=4xAcYEpa97b5Lv9bIo5JHbCO6lhYBnH5EmJ2MrjbU3k,22517
|
|
9
9
|
posthog/poller.py,sha256=jBz5rfH_kn_bBz7wCB46Fpvso4ttx4uzqIZWvXBCFmQ,595
|
|
10
10
|
posthog/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
posthog/request.py,sha256=CaONBN7a5RD8xiSShVMgHEd9XxKWM6ZQTLZypiqABhA,6168
|
|
12
12
|
posthog/types.py,sha256=Dl3aFGX9XUR0wMmK12r2s5Hjan9jL4HpQ9GHpVcEq5U,10207
|
|
13
13
|
posthog/utils.py,sha256=-0w-OLcCaoldkbBebPzQyBzLJSo9G9yBOg8NDVz7La8,16088
|
|
14
|
-
posthog/version.py,sha256=
|
|
14
|
+
posthog/version.py,sha256=qTlEuCWb-slrdA36sMFDZPyA5fyBwCpQaHtWJmuEIgI,87
|
|
15
15
|
posthog/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
posthog/ai/sanitization.py,sha256=owipZ4eJYtd4JTI-CM_klatclXaeaIec3XJBOUfsOnQ,5770
|
|
17
|
-
posthog/ai/types.py,sha256=
|
|
18
|
-
posthog/ai/utils.py,sha256=
|
|
17
|
+
posthog/ai/types.py,sha256=arX98hR1PIPeJ3vFikxTlACIh1xPp6aEUw1gBLcKoB0,3273
|
|
18
|
+
posthog/ai/utils.py,sha256=pMqL_Aydf08EvUuSVJ1SsIpNwaom6qYIoLNOvMBNSHU,21475
|
|
19
19
|
posthog/ai/anthropic/__init__.py,sha256=8nTvETZzkfW-P3zBMmp06GOHs0N-xyOGu7Oa4di_lno,669
|
|
20
|
-
posthog/ai/anthropic/anthropic.py,sha256=
|
|
21
|
-
posthog/ai/anthropic/anthropic_async.py,sha256=
|
|
22
|
-
posthog/ai/anthropic/anthropic_converter.py,sha256=
|
|
20
|
+
posthog/ai/anthropic/anthropic.py,sha256=UWyM6ryl5_VNQImaBi1RHN7tKXwkqaxy4yaXyPSkDp8,8669
|
|
21
|
+
posthog/ai/anthropic/anthropic_async.py,sha256=ppWHiVp4hTl62Zr3jIwXXidOsqhrwx6iHM3ukG7WiPM,8789
|
|
22
|
+
posthog/ai/anthropic/anthropic_converter.py,sha256=prvaxt_R9kn9IbkxG_mLrw4kexT4i6T80U-6yhaZCNk,13053
|
|
23
23
|
posthog/ai/anthropic/anthropic_providers.py,sha256=Q_v7U4wgieIkvii-Bqh4pLx5pEgbrHmgsCG8lUkKb_0,2103
|
|
24
24
|
posthog/ai/gemini/__init__.py,sha256=JV_9-gBR87leHgZW4XAYZP7LSl4YaXeuhqDUpA8HygA,383
|
|
25
|
-
posthog/ai/gemini/gemini.py,sha256
|
|
26
|
-
posthog/ai/gemini/gemini_converter.py,sha256=
|
|
25
|
+
posthog/ai/gemini/gemini.py,sha256=-c2MnBeask6SrAbFZ7XXZ_OMcuglTBRdnFe_ROVgXWQ,14972
|
|
26
|
+
posthog/ai/gemini/gemini_converter.py,sha256=WFF1gzLGk1DId-1yrA9nDYMd9PXgbVsyhU3wgKjAJTE,18731
|
|
27
27
|
posthog/ai/langchain/__init__.py,sha256=9CqAwLynTGj3ASAR80C3PmdTdrYGmu99tz0JL-HPFgI,70
|
|
28
|
-
posthog/ai/langchain/callbacks.py,sha256=
|
|
28
|
+
posthog/ai/langchain/callbacks.py,sha256=5cjBFTNmHYhWxDWSAjIRfHvTebO8M6D5D37CR9vvoAg,30261
|
|
29
29
|
posthog/ai/openai/__init__.py,sha256=u4OuUT7k1NgFj0TrxjuyegOg7a_UA8nAU6a-Hszr0OM,490
|
|
30
|
-
posthog/ai/openai/openai.py,sha256=
|
|
31
|
-
posthog/ai/openai/openai_async.py,sha256=
|
|
32
|
-
posthog/ai/openai/openai_converter.py,sha256=
|
|
30
|
+
posthog/ai/openai/openai.py,sha256=ts95vdvWH7h0TX4FpLLK_wU_7H0MP3eZBEg0S-lsCKw,20127
|
|
31
|
+
posthog/ai/openai/openai_async.py,sha256=Ebd6_H3Zf3wGPycVJd_vOd3ZVoO3Mf3ZV339BExQd6Q,22436
|
|
32
|
+
posthog/ai/openai/openai_converter.py,sha256=_T7Nx5gzGlklbu0iZjj8qaBB4W_IrnSFNUkUOav3TBE,25466
|
|
33
33
|
posthog/ai/openai/openai_providers.py,sha256=zQIFTXHS2-dBKQX7FZxTFo7rIj5iiN7VHm9_2RzuDs8,3941
|
|
34
34
|
posthog/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
-
posthog/integrations/django.py,sha256=
|
|
35
|
+
posthog/integrations/django.py,sha256=9X37yCF-T-MXUsxqkqjBWG3kdgOCyQYYNJQG_ZlwbRg,12633
|
|
36
36
|
posthog/test/__init__.py,sha256=VYgM6xPbJbvS-xhIcDiBRs0MFC9V_jT65uNeerCz_rM,299
|
|
37
37
|
posthog/test/test_before_send.py,sha256=3546WKlk8rF6bhvqhwcxAsjJovDw0Hf8yTvcYGbrhyI,7912
|
|
38
|
-
posthog/test/test_client.py,sha256=
|
|
38
|
+
posthog/test/test_client.py,sha256=F-jUA0uKgHpVLOdnen2j6WSTp6whlJcpZdSecLoREFg,96273
|
|
39
39
|
posthog/test/test_consumer.py,sha256=HRDXSH0IPpCfo5yHs23n-0VzFyGSjWBKLEa8XNtU3_Y,7080
|
|
40
40
|
posthog/test/test_contexts.py,sha256=GDYpQNGhdzyA3--ia3WPao_4dqyLUpkWm1NMVm2L-So,7004
|
|
41
|
-
posthog/test/test_exception_capture.py,sha256=
|
|
41
|
+
posthog/test/test_exception_capture.py,sha256=tit980vqCvAq8W2UGJN-Mcz_BIKI4XZzmEU4y1Y4YaI,8939
|
|
42
42
|
posthog/test/test_feature_flag.py,sha256=yIMJkoRtdJr91Y6Rb0PPlpZWBIR394TgWhccnlf-vYE,6815
|
|
43
43
|
posthog/test/test_feature_flag_result.py,sha256=jbdTgqlFbgvUlAoRWjguk3IvuzXgN2qbfn77gF_SqJU,15871
|
|
44
|
-
posthog/test/test_feature_flags.py,sha256=
|
|
45
|
-
posthog/test/test_module.py,sha256=
|
|
44
|
+
posthog/test/test_feature_flags.py,sha256=JCSFtHhh60WoClmmuBMctSAOchFNqnZPlpfdi37zTMw,217298
|
|
45
|
+
posthog/test/test_module.py,sha256=CERR0dTPGsAmd7YBxK0yKeB2Zr2b_Lv7hNQoeJauc9I,813
|
|
46
46
|
posthog/test/test_request.py,sha256=l19WVyZQc4Iqmh_bpnAFOj4nGRpDK1iO-o5aJDQfFdo,4449
|
|
47
47
|
posthog/test/test_size_limited_dict.py,sha256=Wom7BkzpHmusHilZy0SV3PNzhw7ucuQgqrx86jf8euo,765
|
|
48
48
|
posthog/test/test_types.py,sha256=csLuBiz6RMV36cpg9LVIor4Khq6MfjjGxYXodx5VttY,7586
|
|
49
49
|
posthog/test/test_utils.py,sha256=NUs2bgqrVuMdnKRq52syizgglt5_7wxxZl3dDMun-Tg,9602
|
|
50
|
-
posthog-6.
|
|
51
|
-
posthog-6.
|
|
52
|
-
posthog-6.
|
|
53
|
-
posthog-6.
|
|
54
|
-
posthog-6.
|
|
50
|
+
posthog-6.9.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
|
|
51
|
+
posthog-6.9.0.dist-info/METADATA,sha256=1YBEEJBiVFhOm-4GFjBTC-oWjJH2NOMqpGqWOSUXO5s,6015
|
|
52
|
+
posthog-6.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
53
|
+
posthog-6.9.0.dist-info/top_level.txt,sha256=7FBLsRjIUHVKQsXIhozuI3k-mun1tapp8iZO9EmUPEw,8
|
|
54
|
+
posthog-6.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|