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.
- posthoganalytics/__init__.py +84 -7
- posthoganalytics/ai/anthropic/__init__.py +10 -0
- posthoganalytics/ai/anthropic/anthropic.py +95 -65
- posthoganalytics/ai/anthropic/anthropic_async.py +95 -65
- posthoganalytics/ai/anthropic/anthropic_converter.py +443 -0
- posthoganalytics/ai/gemini/__init__.py +15 -1
- posthoganalytics/ai/gemini/gemini.py +66 -71
- posthoganalytics/ai/gemini/gemini_async.py +423 -0
- posthoganalytics/ai/gemini/gemini_converter.py +652 -0
- posthoganalytics/ai/langchain/callbacks.py +58 -13
- posthoganalytics/ai/openai/__init__.py +16 -1
- posthoganalytics/ai/openai/openai.py +140 -149
- posthoganalytics/ai/openai/openai_async.py +127 -82
- posthoganalytics/ai/openai/openai_converter.py +741 -0
- posthoganalytics/ai/sanitization.py +248 -0
- posthoganalytics/ai/types.py +125 -0
- posthoganalytics/ai/utils.py +339 -356
- posthoganalytics/client.py +345 -97
- posthoganalytics/contexts.py +81 -0
- posthoganalytics/exception_utils.py +250 -2
- posthoganalytics/feature_flags.py +26 -10
- posthoganalytics/flag_definition_cache.py +127 -0
- posthoganalytics/integrations/django.py +157 -19
- posthoganalytics/request.py +203 -23
- posthoganalytics/test/test_client.py +250 -22
- posthoganalytics/test/test_exception_capture.py +418 -0
- posthoganalytics/test/test_feature_flag_result.py +441 -2
- posthoganalytics/test/test_feature_flags.py +308 -104
- posthoganalytics/test/test_flag_definition_cache.py +612 -0
- posthoganalytics/test/test_module.py +0 -8
- posthoganalytics/test/test_request.py +536 -0
- posthoganalytics/test/test_utils.py +4 -1
- posthoganalytics/types.py +40 -0
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/METADATA +12 -12
- posthoganalytics-7.4.3.dist-info/RECORD +57 -0
- posthoganalytics-6.7.0.dist-info/RECORD +0 -49
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/licenses/LICENSE +0 -0
- {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
|