posthoganalytics 6.7.14__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.
- posthoganalytics/__init__.py +29 -2
- posthoganalytics/ai/anthropic/anthropic_async.py +30 -67
- posthoganalytics/ai/anthropic/anthropic_converter.py +40 -0
- posthoganalytics/ai/gemini/gemini_converter.py +73 -3
- posthoganalytics/ai/openai/openai_async.py +19 -0
- posthoganalytics/ai/openai/openai_converter.py +124 -0
- posthoganalytics/ai/types.py +1 -0
- posthoganalytics/ai/utils.py +30 -0
- posthoganalytics/client.py +49 -0
- posthoganalytics/contexts.py +81 -0
- posthoganalytics/exception_utils.py +192 -0
- posthoganalytics/test/test_exception_capture.py +300 -0
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.7.14.dist-info → posthoganalytics-6.9.0.dist-info}/METADATA +1 -1
- {posthoganalytics-6.7.14.dist-info → posthoganalytics-6.9.0.dist-info}/RECORD +18 -18
- {posthoganalytics-6.7.14.dist-info → posthoganalytics-6.9.0.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.7.14.dist-info → posthoganalytics-6.9.0.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-6.7.14.dist-info → posthoganalytics-6.9.0.dist-info}/top_level.txt +0 -0
posthoganalytics/contexts.py
CHANGED
|
@@ -22,6 +22,9 @@ class ContextScope:
|
|
|
22
22
|
self.session_id: Optional[str] = None
|
|
23
23
|
self.distinct_id: Optional[str] = None
|
|
24
24
|
self.tags: Dict[str, Any] = {}
|
|
25
|
+
self.capture_exception_code_variables: Optional[bool] = None
|
|
26
|
+
self.code_variables_mask_patterns: Optional[list] = None
|
|
27
|
+
self.code_variables_ignore_patterns: Optional[list] = None
|
|
25
28
|
|
|
26
29
|
def set_session_id(self, session_id: str):
|
|
27
30
|
self.session_id = session_id
|
|
@@ -32,6 +35,15 @@ class ContextScope:
|
|
|
32
35
|
def add_tag(self, key: str, value: Any):
|
|
33
36
|
self.tags[key] = value
|
|
34
37
|
|
|
38
|
+
def set_capture_exception_code_variables(self, enabled: bool):
|
|
39
|
+
self.capture_exception_code_variables = enabled
|
|
40
|
+
|
|
41
|
+
def set_code_variables_mask_patterns(self, mask_patterns: list):
|
|
42
|
+
self.code_variables_mask_patterns = mask_patterns
|
|
43
|
+
|
|
44
|
+
def set_code_variables_ignore_patterns(self, ignore_patterns: list):
|
|
45
|
+
self.code_variables_ignore_patterns = ignore_patterns
|
|
46
|
+
|
|
35
47
|
def get_parent(self):
|
|
36
48
|
return self.parent
|
|
37
49
|
|
|
@@ -59,6 +71,27 @@ class ContextScope:
|
|
|
59
71
|
tags.update(new_tags)
|
|
60
72
|
return tags
|
|
61
73
|
|
|
74
|
+
def get_capture_exception_code_variables(self) -> Optional[bool]:
|
|
75
|
+
if self.capture_exception_code_variables is not None:
|
|
76
|
+
return self.capture_exception_code_variables
|
|
77
|
+
if self.parent is not None and not self.fresh:
|
|
78
|
+
return self.parent.get_capture_exception_code_variables()
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def get_code_variables_mask_patterns(self) -> Optional[list]:
|
|
82
|
+
if self.code_variables_mask_patterns is not None:
|
|
83
|
+
return self.code_variables_mask_patterns
|
|
84
|
+
if self.parent is not None and not self.fresh:
|
|
85
|
+
return self.parent.get_code_variables_mask_patterns()
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def get_code_variables_ignore_patterns(self) -> Optional[list]:
|
|
89
|
+
if self.code_variables_ignore_patterns is not None:
|
|
90
|
+
return self.code_variables_ignore_patterns
|
|
91
|
+
if self.parent is not None and not self.fresh:
|
|
92
|
+
return self.parent.get_code_variables_ignore_patterns()
|
|
93
|
+
return None
|
|
94
|
+
|
|
62
95
|
|
|
63
96
|
_context_stack: contextvars.ContextVar[Optional[ContextScope]] = contextvars.ContextVar(
|
|
64
97
|
"posthog_context_stack", default=None
|
|
@@ -243,6 +276,54 @@ def get_context_distinct_id() -> Optional[str]:
|
|
|
243
276
|
return None
|
|
244
277
|
|
|
245
278
|
|
|
279
|
+
def set_capture_exception_code_variables_context(enabled: bool) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Set whether code variables are captured for the current context.
|
|
282
|
+
"""
|
|
283
|
+
current_context = _get_current_context()
|
|
284
|
+
if current_context:
|
|
285
|
+
current_context.set_capture_exception_code_variables(enabled)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def set_code_variables_mask_patterns_context(mask_patterns: list) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Variable names matching these patterns will be masked with *** when capturing code variables.
|
|
291
|
+
"""
|
|
292
|
+
current_context = _get_current_context()
|
|
293
|
+
if current_context:
|
|
294
|
+
current_context.set_code_variables_mask_patterns(mask_patterns)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def set_code_variables_ignore_patterns_context(ignore_patterns: list) -> None:
|
|
298
|
+
"""
|
|
299
|
+
Variable names matching these patterns will be ignored completely when capturing code variables.
|
|
300
|
+
"""
|
|
301
|
+
current_context = _get_current_context()
|
|
302
|
+
if current_context:
|
|
303
|
+
current_context.set_code_variables_ignore_patterns(ignore_patterns)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def get_capture_exception_code_variables_context() -> Optional[bool]:
|
|
307
|
+
current_context = _get_current_context()
|
|
308
|
+
if current_context:
|
|
309
|
+
return current_context.get_capture_exception_code_variables()
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_code_variables_mask_patterns_context() -> Optional[list]:
|
|
314
|
+
current_context = _get_current_context()
|
|
315
|
+
if current_context:
|
|
316
|
+
return current_context.get_code_variables_mask_patterns()
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def get_code_variables_ignore_patterns_context() -> Optional[list]:
|
|
321
|
+
current_context = _get_current_context()
|
|
322
|
+
if current_context:
|
|
323
|
+
return current_context.get_code_variables_ignore_patterns()
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
246
327
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
247
328
|
|
|
248
329
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# 💖open source (under MIT License)
|
|
6
6
|
# We want to keep payloads as similar to Sentry as possible for easy interoperability
|
|
7
7
|
|
|
8
|
+
import json
|
|
8
9
|
import linecache
|
|
9
10
|
import os
|
|
10
11
|
import re
|
|
@@ -26,6 +27,7 @@ from typing import ( # noqa: F401
|
|
|
26
27
|
Union,
|
|
27
28
|
cast,
|
|
28
29
|
TYPE_CHECKING,
|
|
30
|
+
Pattern,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
from posthoganalytics.args import ExcInfo, ExceptionArg # noqa: F401
|
|
@@ -40,6 +42,42 @@ except ImportError:
|
|
|
40
42
|
|
|
41
43
|
DEFAULT_MAX_VALUE_LENGTH = 1024
|
|
42
44
|
|
|
45
|
+
DEFAULT_CODE_VARIABLES_MASK_PATTERNS = [
|
|
46
|
+
r"(?i).*password.*",
|
|
47
|
+
r"(?i).*secret.*",
|
|
48
|
+
r"(?i).*passwd.*",
|
|
49
|
+
r"(?i).*pwd.*",
|
|
50
|
+
r"(?i).*api_key.*",
|
|
51
|
+
r"(?i).*apikey.*",
|
|
52
|
+
r"(?i).*auth.*",
|
|
53
|
+
r"(?i).*credentials.*",
|
|
54
|
+
r"(?i).*privatekey.*",
|
|
55
|
+
r"(?i).*private_key.*",
|
|
56
|
+
r"(?i).*token.*",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS = [r"^__.*"]
|
|
60
|
+
|
|
61
|
+
CODE_VARIABLES_REDACTED_VALUE = "$$_posthog_redacted_based_on_masking_rules_$$"
|
|
62
|
+
|
|
63
|
+
DEFAULT_TOTAL_VARIABLES_SIZE_LIMIT = 20 * 1024
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class VariableSizeLimiter:
|
|
67
|
+
def __init__(self, max_size=DEFAULT_TOTAL_VARIABLES_SIZE_LIMIT):
|
|
68
|
+
self.max_size = max_size
|
|
69
|
+
self.current_size = 0
|
|
70
|
+
|
|
71
|
+
def can_add(self, size):
|
|
72
|
+
return self.current_size + size <= self.max_size
|
|
73
|
+
|
|
74
|
+
def add(self, size):
|
|
75
|
+
self.current_size += size
|
|
76
|
+
|
|
77
|
+
def get_remaining_space(self):
|
|
78
|
+
return self.max_size - self.current_size
|
|
79
|
+
|
|
80
|
+
|
|
43
81
|
LogLevelStr = Literal["fatal", "critical", "error", "warning", "info", "debug"]
|
|
44
82
|
|
|
45
83
|
Event = TypedDict(
|
|
@@ -884,3 +922,157 @@ def strip_string(value, max_length=None):
|
|
|
884
922
|
"rem": [["!limit", "x", max_length - 3, max_length]],
|
|
885
923
|
},
|
|
886
924
|
)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _compile_patterns(patterns):
|
|
928
|
+
compiled = []
|
|
929
|
+
for pattern in patterns:
|
|
930
|
+
try:
|
|
931
|
+
compiled.append(re.compile(pattern))
|
|
932
|
+
except:
|
|
933
|
+
pass
|
|
934
|
+
return compiled
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _pattern_matches(name, patterns):
|
|
938
|
+
for pattern in patterns:
|
|
939
|
+
if pattern.search(name):
|
|
940
|
+
return True
|
|
941
|
+
return False
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _serialize_variable_value(value, limiter, max_length=1024):
|
|
945
|
+
try:
|
|
946
|
+
if value is None:
|
|
947
|
+
result = "None"
|
|
948
|
+
elif isinstance(value, bool):
|
|
949
|
+
result = str(value)
|
|
950
|
+
elif isinstance(value, (int, float)):
|
|
951
|
+
result_size = len(str(value))
|
|
952
|
+
if not limiter.can_add(result_size):
|
|
953
|
+
return None
|
|
954
|
+
limiter.add(result_size)
|
|
955
|
+
return value
|
|
956
|
+
elif isinstance(value, str):
|
|
957
|
+
result = value
|
|
958
|
+
else:
|
|
959
|
+
result = json.dumps(value)
|
|
960
|
+
|
|
961
|
+
if len(result) > max_length:
|
|
962
|
+
result = result[: max_length - 3] + "..."
|
|
963
|
+
|
|
964
|
+
result_size = len(result)
|
|
965
|
+
if not limiter.can_add(result_size):
|
|
966
|
+
return None
|
|
967
|
+
limiter.add(result_size)
|
|
968
|
+
|
|
969
|
+
return result
|
|
970
|
+
except Exception:
|
|
971
|
+
try:
|
|
972
|
+
fallback = f"<{type(value).__name__}>"
|
|
973
|
+
fallback_size = len(fallback)
|
|
974
|
+
if not limiter.can_add(fallback_size):
|
|
975
|
+
return None
|
|
976
|
+
limiter.add(fallback_size)
|
|
977
|
+
return fallback
|
|
978
|
+
except Exception:
|
|
979
|
+
fallback = "<unserializable object>"
|
|
980
|
+
fallback_size = len(fallback)
|
|
981
|
+
if not limiter.can_add(fallback_size):
|
|
982
|
+
return None
|
|
983
|
+
limiter.add(fallback_size)
|
|
984
|
+
return fallback
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _is_simple_type(value):
|
|
988
|
+
return isinstance(value, (type(None), bool, int, float, str))
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def serialize_code_variables(
|
|
992
|
+
frame, limiter, mask_patterns=None, ignore_patterns=None, max_length=1024
|
|
993
|
+
):
|
|
994
|
+
if mask_patterns is None:
|
|
995
|
+
mask_patterns = []
|
|
996
|
+
if ignore_patterns is None:
|
|
997
|
+
ignore_patterns = []
|
|
998
|
+
|
|
999
|
+
compiled_mask = _compile_patterns(mask_patterns)
|
|
1000
|
+
compiled_ignore = _compile_patterns(ignore_patterns)
|
|
1001
|
+
|
|
1002
|
+
try:
|
|
1003
|
+
local_vars = frame.f_locals.copy()
|
|
1004
|
+
except Exception:
|
|
1005
|
+
return {}
|
|
1006
|
+
|
|
1007
|
+
simple_vars = {}
|
|
1008
|
+
complex_vars = {}
|
|
1009
|
+
|
|
1010
|
+
for name, value in local_vars.items():
|
|
1011
|
+
if _pattern_matches(name, compiled_ignore):
|
|
1012
|
+
continue
|
|
1013
|
+
|
|
1014
|
+
if _is_simple_type(value):
|
|
1015
|
+
simple_vars[name] = value
|
|
1016
|
+
else:
|
|
1017
|
+
complex_vars[name] = value
|
|
1018
|
+
|
|
1019
|
+
result = {}
|
|
1020
|
+
|
|
1021
|
+
all_vars = {**simple_vars, **complex_vars}
|
|
1022
|
+
ordered_names = list(sorted(simple_vars.keys())) + list(sorted(complex_vars.keys()))
|
|
1023
|
+
|
|
1024
|
+
for name in ordered_names:
|
|
1025
|
+
value = all_vars[name]
|
|
1026
|
+
|
|
1027
|
+
if _pattern_matches(name, compiled_mask):
|
|
1028
|
+
redacted_value = CODE_VARIABLES_REDACTED_VALUE
|
|
1029
|
+
redacted_size = len(redacted_value)
|
|
1030
|
+
if not limiter.can_add(redacted_size):
|
|
1031
|
+
break
|
|
1032
|
+
limiter.add(redacted_size)
|
|
1033
|
+
result[name] = redacted_value
|
|
1034
|
+
else:
|
|
1035
|
+
serialized = _serialize_variable_value(value, limiter, max_length)
|
|
1036
|
+
if serialized is None:
|
|
1037
|
+
break
|
|
1038
|
+
result[name] = serialized
|
|
1039
|
+
|
|
1040
|
+
return result
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def try_attach_code_variables_to_frames(
|
|
1044
|
+
all_exceptions, exc_info, mask_patterns, ignore_patterns
|
|
1045
|
+
):
|
|
1046
|
+
exc_type, exc_value, traceback = exc_info
|
|
1047
|
+
|
|
1048
|
+
if traceback is None:
|
|
1049
|
+
return
|
|
1050
|
+
|
|
1051
|
+
tb_frames = list(iter_stacks(traceback))
|
|
1052
|
+
|
|
1053
|
+
if not tb_frames:
|
|
1054
|
+
return
|
|
1055
|
+
|
|
1056
|
+
limiter = VariableSizeLimiter()
|
|
1057
|
+
|
|
1058
|
+
for exception in all_exceptions:
|
|
1059
|
+
stacktrace = exception.get("stacktrace")
|
|
1060
|
+
if not stacktrace or "frames" not in stacktrace:
|
|
1061
|
+
continue
|
|
1062
|
+
|
|
1063
|
+
serialized_frames = stacktrace["frames"]
|
|
1064
|
+
|
|
1065
|
+
for serialized_frame, tb_item in zip(serialized_frames, tb_frames):
|
|
1066
|
+
if not serialized_frame.get("in_app"):
|
|
1067
|
+
continue
|
|
1068
|
+
|
|
1069
|
+
variables = serialize_code_variables(
|
|
1070
|
+
tb_item.tb_frame,
|
|
1071
|
+
limiter,
|
|
1072
|
+
mask_patterns=mask_patterns,
|
|
1073
|
+
ignore_patterns=ignore_patterns,
|
|
1074
|
+
max_length=1024,
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
if variables:
|
|
1078
|
+
serialized_frame["code_variables"] = variables
|
|
@@ -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 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_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 posthoganalytics 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 posthoganalytics 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 posthoganalytics 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 posthoganalytics 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
|
posthoganalytics/version.py
CHANGED