posthoganalytics 6.7.0__py3-none-any.whl → 7.4.3__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.
Files changed (40) hide show
  1. posthoganalytics/__init__.py +84 -7
  2. posthoganalytics/ai/anthropic/__init__.py +10 -0
  3. posthoganalytics/ai/anthropic/anthropic.py +95 -65
  4. posthoganalytics/ai/anthropic/anthropic_async.py +95 -65
  5. posthoganalytics/ai/anthropic/anthropic_converter.py +443 -0
  6. posthoganalytics/ai/gemini/__init__.py +15 -1
  7. posthoganalytics/ai/gemini/gemini.py +66 -71
  8. posthoganalytics/ai/gemini/gemini_async.py +423 -0
  9. posthoganalytics/ai/gemini/gemini_converter.py +652 -0
  10. posthoganalytics/ai/langchain/callbacks.py +58 -13
  11. posthoganalytics/ai/openai/__init__.py +16 -1
  12. posthoganalytics/ai/openai/openai.py +140 -149
  13. posthoganalytics/ai/openai/openai_async.py +127 -82
  14. posthoganalytics/ai/openai/openai_converter.py +741 -0
  15. posthoganalytics/ai/sanitization.py +248 -0
  16. posthoganalytics/ai/types.py +125 -0
  17. posthoganalytics/ai/utils.py +339 -356
  18. posthoganalytics/client.py +345 -97
  19. posthoganalytics/contexts.py +81 -0
  20. posthoganalytics/exception_utils.py +250 -2
  21. posthoganalytics/feature_flags.py +26 -10
  22. posthoganalytics/flag_definition_cache.py +127 -0
  23. posthoganalytics/integrations/django.py +157 -19
  24. posthoganalytics/request.py +203 -23
  25. posthoganalytics/test/test_client.py +250 -22
  26. posthoganalytics/test/test_exception_capture.py +418 -0
  27. posthoganalytics/test/test_feature_flag_result.py +441 -2
  28. posthoganalytics/test/test_feature_flags.py +308 -104
  29. posthoganalytics/test/test_flag_definition_cache.py +612 -0
  30. posthoganalytics/test/test_module.py +0 -8
  31. posthoganalytics/test/test_request.py +536 -0
  32. posthoganalytics/test/test_utils.py +4 -1
  33. posthoganalytics/types.py +40 -0
  34. posthoganalytics/version.py +1 -1
  35. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/METADATA +12 -12
  36. posthoganalytics-7.4.3.dist-info/RECORD +57 -0
  37. posthoganalytics-6.7.0.dist-info/RECORD +0 -49
  38. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/WHEEL +0 -0
  39. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/licenses/LICENSE +0 -0
  40. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/top_level.txt +0 -0
@@ -32,3 +32,421 @@ 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 posthoganalytics 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_sensitive_dict = {
63
+ "safe_key": "safe_value",
64
+ "password": "secret123", # key matches pattern -> should be masked
65
+ "other_key": "contains_password_here", # value matches pattern -> should be masked
66
+ }
67
+ my_nested_dict = {
68
+ "level1": {
69
+ "level2": {
70
+ "api_key": "nested_secret", # deeply nested key matches
71
+ "data": "contains_token_here", # deeply nested value matches
72
+ "safe": "visible",
73
+ }
74
+ }
75
+ }
76
+ my_list = ["safe_item", "has_password_inside", "another_safe"]
77
+ my_tuple = ("tuple_safe", "secret_in_value", "tuple_also_safe")
78
+ my_list_of_dicts = [
79
+ {"id": 1, "password": "list_dict_secret"},
80
+ {"id": 2, "value": "safe_value"},
81
+ ]
82
+ my_obj = UnserializableObject()
83
+ my_password = "secret123" # Should be masked by default (name matches)
84
+ my_innocent_var = "contains_password_here" # Should be masked by default (value matches)
85
+ __should_be_ignored = "hidden" # Should be ignored by default
86
+
87
+ 1/0 # Trigger exception
88
+
89
+ def intermediate_function():
90
+ request_id = "abc-123"
91
+ user_count = 100
92
+ is_active = True
93
+
94
+ trigger_error()
95
+
96
+ def process_data():
97
+ batch_size = 50
98
+ retry_count = 3
99
+
100
+ intermediate_function()
101
+
102
+ process_data()
103
+ """
104
+ )
105
+ )
106
+
107
+ with pytest.raises(subprocess.CalledProcessError) as excinfo:
108
+ subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
109
+
110
+ output = excinfo.value.output
111
+
112
+ assert b"ZeroDivisionError" in output
113
+ assert b"code_variables" in output
114
+
115
+ # Variables from trigger_error frame
116
+ assert b"'my_string': 'hello world'" in output
117
+ assert b"'my_number': 42" in output
118
+ assert b"'my_bool': 'True'" in output
119
+ assert b'"my_dict": "{\\"name\\": \\"test\\", \\"value\\": 123}"' in output
120
+ assert (
121
+ b'{\\"safe_key\\": \\"safe_value\\", \\"password\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"other_key\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\"}'
122
+ in output
123
+ )
124
+ assert (
125
+ b'{\\"level1\\": {\\"level2\\": {\\"api_key\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"data\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"safe\\": \\"visible\\"}}}'
126
+ in output
127
+ )
128
+ assert (
129
+ b'[\\"safe_item\\", \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"another_safe\\"]'
130
+ in output
131
+ )
132
+ assert (
133
+ b'[\\"tuple_safe\\", \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"tuple_also_safe\\"]'
134
+ in output
135
+ )
136
+ assert (
137
+ b'[{\\"id\\": 1, \\"password\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\"}, {\\"id\\": 2, \\"value\\": \\"safe_value\\"}]'
138
+ in output
139
+ )
140
+ assert b"<__main__.UnserializableObject object at" in output
141
+ assert b"'my_password': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
142
+ assert (
143
+ b"'my_innocent_var': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
144
+ )
145
+ assert b"'__should_be_ignored':" not in output
146
+
147
+ # Variables from intermediate_function frame
148
+ assert b"'request_id': 'abc-123'" in output
149
+ assert b"'user_count': 100" in output
150
+ assert b"'is_active': 'True'" in output
151
+
152
+ # Variables from process_data frame
153
+ assert b"'batch_size': 50" in output
154
+ assert b"'retry_count': 3" in output
155
+
156
+
157
+ def test_code_variables_context_override(tmpdir):
158
+ app = tmpdir.join("app.py")
159
+ app.write(
160
+ dedent(
161
+ """
162
+ import os
163
+ import posthog
164
+ from posthoganalytics import Posthog
165
+
166
+ posthog_client = Posthog(
167
+ 'phc_x',
168
+ host='https://eu.i.posthog.com',
169
+ debug=True,
170
+ enable_exception_autocapture=True,
171
+ capture_exception_code_variables=False,
172
+ project_root=os.path.dirname(os.path.abspath(__file__))
173
+ )
174
+
175
+ def process_data():
176
+ bank = "should_be_masked"
177
+ __dunder_var = "should_be_visible"
178
+
179
+ 1/0
180
+
181
+ with posthog.new_context(client=posthog_client):
182
+ posthog.set_capture_exception_code_variables_context(True)
183
+ posthog.set_code_variables_mask_patterns_context([r"(?i).*bank.*"])
184
+ posthog.set_code_variables_ignore_patterns_context([])
185
+
186
+ process_data()
187
+ """
188
+ )
189
+ )
190
+
191
+ with pytest.raises(subprocess.CalledProcessError) as excinfo:
192
+ subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
193
+
194
+ output = excinfo.value.output
195
+
196
+ assert b"ZeroDivisionError" in output
197
+ assert b"code_variables" in output
198
+ assert b"'bank': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
199
+ assert b"'__dunder_var': 'should_be_visible'" in output
200
+
201
+
202
+ def test_code_variables_size_limiter(tmpdir):
203
+ app = tmpdir.join("app.py")
204
+ app.write(
205
+ dedent(
206
+ """
207
+ import os
208
+ from posthoganalytics import Posthog
209
+
210
+ posthog = Posthog(
211
+ 'phc_x',
212
+ host='https://eu.i.posthog.com',
213
+ debug=True,
214
+ enable_exception_autocapture=True,
215
+ capture_exception_code_variables=True,
216
+ project_root=os.path.dirname(os.path.abspath(__file__))
217
+ )
218
+
219
+ def trigger_error():
220
+ var_a = "a" * 2000
221
+ var_b = "b" * 2000
222
+ var_c = "c" * 2000
223
+ var_d = "d" * 2000
224
+ var_e = "e" * 2000
225
+ var_f = "f" * 2000
226
+ var_g = "g" * 2000
227
+
228
+ 1/0
229
+
230
+ def intermediate_function():
231
+ var_h = "h" * 2000
232
+ var_i = "i" * 2000
233
+ var_j = "j" * 2000
234
+ var_k = "k" * 2000
235
+ var_l = "l" * 2000
236
+ var_m = "m" * 2000
237
+ var_n = "n" * 2000
238
+
239
+ trigger_error()
240
+
241
+ def process_data():
242
+ var_o = "o" * 2000
243
+ var_p = "p" * 2000
244
+ var_q = "q" * 2000
245
+ var_r = "r" * 2000
246
+ var_s = "s" * 2000
247
+ var_t = "t" * 2000
248
+ var_u = "u" * 2000
249
+
250
+ intermediate_function()
251
+
252
+ process_data()
253
+ """
254
+ )
255
+ )
256
+
257
+ with pytest.raises(subprocess.CalledProcessError) as excinfo:
258
+ subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
259
+
260
+ output = excinfo.value.output.decode("utf-8")
261
+
262
+ assert "ZeroDivisionError" in output
263
+ assert "code_variables" in output
264
+
265
+ captured_vars = []
266
+ for var_name in [
267
+ "var_a",
268
+ "var_b",
269
+ "var_c",
270
+ "var_d",
271
+ "var_e",
272
+ "var_f",
273
+ "var_g",
274
+ "var_h",
275
+ "var_i",
276
+ "var_j",
277
+ "var_k",
278
+ "var_l",
279
+ "var_m",
280
+ "var_n",
281
+ "var_o",
282
+ "var_p",
283
+ "var_q",
284
+ "var_r",
285
+ "var_s",
286
+ "var_t",
287
+ "var_u",
288
+ ]:
289
+ if f"'{var_name}'" in output:
290
+ captured_vars.append(var_name)
291
+
292
+ assert len(captured_vars) > 0
293
+ assert len(captured_vars) < 21
294
+
295
+
296
+ def test_code_variables_disabled_capture(tmpdir):
297
+ app = tmpdir.join("app.py")
298
+ app.write(
299
+ dedent(
300
+ """
301
+ import os
302
+ from posthoganalytics import Posthog
303
+
304
+ posthog = Posthog(
305
+ 'phc_x',
306
+ host='https://eu.i.posthog.com',
307
+ debug=True,
308
+ enable_exception_autocapture=True,
309
+ capture_exception_code_variables=False,
310
+ project_root=os.path.dirname(os.path.abspath(__file__))
311
+ )
312
+
313
+ def trigger_error():
314
+ my_string = "hello world"
315
+ my_number = 42
316
+ my_bool = True
317
+
318
+ 1/0
319
+
320
+ trigger_error()
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_string'" not in output
334
+ assert "'my_number'" not in output
335
+
336
+
337
+ def test_code_variables_enabled_then_disabled_in_context(tmpdir):
338
+ app = tmpdir.join("app.py")
339
+ app.write(
340
+ dedent(
341
+ """
342
+ import os
343
+ import posthog
344
+ from posthoganalytics import Posthog
345
+
346
+ posthog_client = Posthog(
347
+ 'phc_x',
348
+ host='https://eu.i.posthog.com',
349
+ debug=True,
350
+ enable_exception_autocapture=True,
351
+ capture_exception_code_variables=True,
352
+ project_root=os.path.dirname(os.path.abspath(__file__))
353
+ )
354
+
355
+ def process_data():
356
+ my_var = "should not be captured"
357
+ important_value = 123
358
+
359
+ 1/0
360
+
361
+ with posthog.new_context(client=posthog_client):
362
+ posthog.set_capture_exception_code_variables_context(False)
363
+
364
+ process_data()
365
+ """
366
+ )
367
+ )
368
+
369
+ with pytest.raises(subprocess.CalledProcessError) as excinfo:
370
+ subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
371
+
372
+ output = excinfo.value.output.decode("utf-8")
373
+
374
+ assert "ZeroDivisionError" in output
375
+ assert "'code_variables':" not in output
376
+ assert '"code_variables":' not in output
377
+ assert "'my_var'" not in output
378
+ assert "'important_value'" not in output
379
+
380
+
381
+ def test_code_variables_repr_fallback(tmpdir):
382
+ app = tmpdir.join("app.py")
383
+ app.write(
384
+ dedent(
385
+ """
386
+ import os
387
+ import re
388
+ from datetime import datetime, timedelta
389
+ from decimal import Decimal
390
+ from fractions import Fraction
391
+ from posthoganalytics import Posthog
392
+
393
+ class CustomReprClass:
394
+ def __repr__(self):
395
+ return '<CustomReprClass: custom representation>'
396
+
397
+ posthog = Posthog(
398
+ 'phc_x',
399
+ host='https://eu.i.posthog.com',
400
+ debug=True,
401
+ enable_exception_autocapture=True,
402
+ capture_exception_code_variables=True,
403
+ project_root=os.path.dirname(os.path.abspath(__file__))
404
+ )
405
+
406
+ def trigger_error():
407
+ my_regex = re.compile(r'\\d+')
408
+ my_datetime = datetime(2024, 1, 15, 10, 30, 45)
409
+ my_timedelta = timedelta(days=5, hours=3)
410
+ my_decimal = Decimal('123.456')
411
+ my_fraction = Fraction(3, 4)
412
+ my_set = {1, 2, 3}
413
+ my_frozenset = frozenset([4, 5, 6])
414
+ my_bytes = b'hello bytes'
415
+ my_bytearray = bytearray(b'mutable bytes')
416
+ my_memoryview = memoryview(b'memory view')
417
+ my_complex = complex(3, 4)
418
+ my_range = range(10)
419
+ my_custom = CustomReprClass()
420
+ my_lambda = lambda x: x * 2
421
+ my_function = trigger_error
422
+
423
+ 1/0
424
+
425
+ trigger_error()
426
+ """
427
+ )
428
+ )
429
+
430
+ with pytest.raises(subprocess.CalledProcessError) as excinfo:
431
+ subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
432
+
433
+ output = excinfo.value.output.decode("utf-8")
434
+
435
+ assert "ZeroDivisionError" in output
436
+ assert "code_variables" in output
437
+
438
+ assert "re.compile(" in output and "\\\\d+" in output
439
+ assert "datetime.datetime(2024, 1, 15, 10, 30, 45)" in output
440
+ assert "datetime.timedelta(days=5, seconds=10800)" in output
441
+ assert "Decimal('123.456')" in output
442
+ assert "Fraction(3, 4)" in output
443
+ assert "{1, 2, 3}" in output
444
+ assert "frozenset({4, 5, 6})" in output
445
+ assert "b'hello bytes'" in output
446
+ assert "bytearray(b'mutable bytes')" in output
447
+ assert "<memory at" in output
448
+ assert "(3+4j)" in output
449
+ assert "range(0, 10)" in output
450
+ assert "<CustomReprClass: custom representation>" in output
451
+ assert "<lambda>" in output
452
+ assert "<function trigger_error at" in output