posthoganalytics 6.8.0__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.
@@ -10,6 +10,9 @@ from posthoganalytics.contexts import (
10
10
  tag as inner_tag,
11
11
  set_context_session as inner_set_context_session,
12
12
  identify_context as inner_identify_context,
13
+ set_capture_exception_code_variables_context as inner_set_capture_exception_code_variables_context,
14
+ set_code_variables_mask_patterns_context as inner_set_code_variables_mask_patterns_context,
15
+ set_code_variables_ignore_patterns_context as inner_set_code_variables_ignore_patterns_context,
13
16
  )
14
17
  from posthoganalytics.feature_flags import InconclusiveMatchError, RequiresServerEvaluation
15
18
  from posthoganalytics.types import FeatureFlag, FlagsAndPayloads, FeatureFlagResult
@@ -20,13 +23,14 @@ __version__ = VERSION
20
23
  """Context management."""
21
24
 
22
25
 
23
- def new_context(fresh=False, capture_exceptions=True):
26
+ def new_context(fresh=False, capture_exceptions=True, client=None):
24
27
  """
25
28
  Create a new context scope that will be active for the duration of the with block.
26
29
 
27
30
  Args:
28
31
  fresh: Whether to start with a fresh context (default: False)
29
32
  capture_exceptions: Whether to capture exceptions raised within the context (default: True)
33
+ client: Optional Posthog client instance to use for this context (default: None)
30
34
 
31
35
  Examples:
32
36
  ```python
@@ -39,7 +43,9 @@ def new_context(fresh=False, capture_exceptions=True):
39
43
  Category:
40
44
  Contexts
41
45
  """
42
- return inner_new_context(fresh=fresh, capture_exceptions=capture_exceptions)
46
+ return inner_new_context(
47
+ fresh=fresh, capture_exceptions=capture_exceptions, client=client
48
+ )
43
49
 
44
50
 
45
51
  def scoped(fresh=False, capture_exceptions=True):
@@ -103,6 +109,27 @@ def identify_context(distinct_id: str):
103
109
  return inner_identify_context(distinct_id)
104
110
 
105
111
 
112
+ def set_capture_exception_code_variables_context(enabled: bool):
113
+ """
114
+ Set whether code variables are captured for the current context.
115
+ """
116
+ return inner_set_capture_exception_code_variables_context(enabled)
117
+
118
+
119
+ def set_code_variables_mask_patterns_context(mask_patterns: list):
120
+ """
121
+ Variable names matching these patterns will be masked with *** when capturing code variables.
122
+ """
123
+ return inner_set_code_variables_mask_patterns_context(mask_patterns)
124
+
125
+
126
+ def set_code_variables_ignore_patterns_context(ignore_patterns: list):
127
+ """
128
+ Variable names matching these patterns will be ignored completely when capturing code variables.
129
+ """
130
+ return inner_set_code_variables_ignore_patterns_context(ignore_patterns)
131
+
132
+
106
133
  def tag(name: str, value: Any):
107
134
  """
108
135
  Add a tag to the current context.
@@ -19,6 +19,9 @@ from posthoganalytics.exception_utils import (
19
19
  handle_in_app,
20
20
  exception_is_already_captured,
21
21
  mark_exception_as_captured,
22
+ try_attach_code_variables_to_frames,
23
+ DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
24
+ DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
22
25
  )
23
26
  from posthoganalytics.feature_flags import (
24
27
  InconclusiveMatchError,
@@ -39,6 +42,9 @@ from posthoganalytics.contexts import (
39
42
  _get_current_context,
40
43
  get_context_distinct_id,
41
44
  get_context_session_id,
45
+ get_capture_exception_code_variables_context,
46
+ get_code_variables_mask_patterns_context,
47
+ get_code_variables_ignore_patterns_context,
42
48
  new_context,
43
49
  )
44
50
  from posthoganalytics.types import (
@@ -178,6 +184,9 @@ class Client(object):
178
184
  before_send=None,
179
185
  flag_fallback_cache_url=None,
180
186
  enable_local_evaluation=True,
187
+ capture_exception_code_variables=False,
188
+ code_variables_mask_patterns=None,
189
+ code_variables_ignore_patterns=None,
181
190
  ):
182
191
  """
183
192
  Initialize a new PostHog client instance.
@@ -233,6 +242,18 @@ class Client(object):
233
242
  self.privacy_mode = privacy_mode
234
243
  self.enable_local_evaluation = enable_local_evaluation
235
244
 
245
+ self.capture_exception_code_variables = capture_exception_code_variables
246
+ self.code_variables_mask_patterns = (
247
+ code_variables_mask_patterns
248
+ if code_variables_mask_patterns is not None
249
+ else DEFAULT_CODE_VARIABLES_MASK_PATTERNS
250
+ )
251
+ self.code_variables_ignore_patterns = (
252
+ code_variables_ignore_patterns
253
+ if code_variables_ignore_patterns is not None
254
+ else DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS
255
+ )
256
+
236
257
  if project_root is None:
237
258
  try:
238
259
  project_root = os.getcwd()
@@ -979,6 +1000,34 @@ class Client(object):
979
1000
  **properties,
980
1001
  }
981
1002
 
1003
+ context_enabled = get_capture_exception_code_variables_context()
1004
+ context_mask = get_code_variables_mask_patterns_context()
1005
+ context_ignore = get_code_variables_ignore_patterns_context()
1006
+
1007
+ enabled = (
1008
+ context_enabled
1009
+ if context_enabled is not None
1010
+ else self.capture_exception_code_variables
1011
+ )
1012
+ mask_patterns = (
1013
+ context_mask
1014
+ if context_mask is not None
1015
+ else self.code_variables_mask_patterns
1016
+ )
1017
+ ignore_patterns = (
1018
+ context_ignore
1019
+ if context_ignore is not None
1020
+ else self.code_variables_ignore_patterns
1021
+ )
1022
+
1023
+ if enabled:
1024
+ try_attach_code_variables_to_frames(
1025
+ all_exceptions_with_trace_and_in_app,
1026
+ exc_info,
1027
+ mask_patterns=mask_patterns,
1028
+ ignore_patterns=ignore_patterns,
1029
+ )
1030
+
982
1031
  if self.log_captured_exceptions:
983
1032
  self.log.exception(exception, extra=kwargs)
984
1033
 
@@ -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
@@ -1,4 +1,4 @@
1
- VERSION = "6.8.0"
1
+ VERSION = "6.9.0"
2
2
 
3
3
  if __name__ == "__main__":
4
4
  print(VERSION, end="") # noqa: T201
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: posthoganalytics
3
- Version: 6.8.0
3
+ Version: 6.9.0
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -1,17 +1,17 @@
1
- posthoganalytics/__init__.py,sha256=vYBBQuWxyCdN2mkFuJgHqGGh0ZcO7WriFy7tEILtpSI,26079
1
+ posthoganalytics/__init__.py,sha256=_zAqJWGGR_saWREp2GioAaZM99qoZyXPB8GXct-z_3U,27256
2
2
  posthoganalytics/args.py,sha256=iZ2JWeANiAREJKhS-Qls9tIngjJOSfAVR8C4xFT5sHw,3307
3
- posthoganalytics/client.py,sha256=GQJSIagCiavxrKeTA1HDTiGmvmwDdtI0pq9Dnr71Mgs,72722
3
+ posthoganalytics/client.py,sha256=8QTaZN84U_NyWCpNMnO7yQtxYw4vfMJ0ixXVikA6FCg,74670
4
4
  posthoganalytics/consumer.py,sha256=CiNbJBdyW9jER3ZYCKbX-JFmEDXlE1lbDy1MSl43-a0,4617
5
- posthoganalytics/contexts.py,sha256=LFSFIYpUFWKTBnGMjV9n1aYHWbAzz5zLJGr2qG34PoE,9405
5
+ posthoganalytics/contexts.py,sha256=Qj8eprL71IVGo4nMtHCs7kIEhezOmxfkYpiPTg-rWjU,12618
6
6
  posthoganalytics/exception_capture.py,sha256=1VHBfffrXXrkK0PT8iVgKPpj_R1pGAzG5f3Qw0WF79w,1783
7
- posthoganalytics/exception_utils.py,sha256=P_75873Y2jayqlLiIkbxCNE7Bc8cM6J9kfrdZ5ZSnA0,26696
7
+ posthoganalytics/exception_utils.py,sha256=DrKNkZdgYXOPtEG5qPvqlPE86nefBc9yazSS5akA34Y,31857
8
8
  posthoganalytics/feature_flags.py,sha256=yHjiH6LSvhQgurbsPCHUdGakZKvkzOLdqB8vL3iyhmw,22544
9
9
  posthoganalytics/poller.py,sha256=jBz5rfH_kn_bBz7wCB46Fpvso4ttx4uzqIZWvXBCFmQ,595
10
10
  posthoganalytics/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  posthoganalytics/request.py,sha256=Bsl2c5WwONKPQzwWMmKPX5VgOlwSiIcSNfhXgoz62Y8,6186
12
12
  posthoganalytics/types.py,sha256=Dl3aFGX9XUR0wMmK12r2s5Hjan9jL4HpQ9GHpVcEq5U,10207
13
13
  posthoganalytics/utils.py,sha256=-0w-OLcCaoldkbBebPzQyBzLJSo9G9yBOg8NDVz7La8,16088
14
- posthoganalytics/version.py,sha256=xqPIL3Vi5H0V2bZOTQqSHr-lQXEhP8VBNZcmOrTSRUw,87
14
+ posthoganalytics/version.py,sha256=qTlEuCWb-slrdA36sMFDZPyA5fyBwCpQaHtWJmuEIgI,87
15
15
  posthoganalytics/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  posthoganalytics/ai/sanitization.py,sha256=owipZ4eJYtd4JTI-CM_klatclXaeaIec3XJBOUfsOnQ,5770
17
17
  posthoganalytics/ai/types.py,sha256=arX98hR1PIPeJ3vFikxTlACIh1xPp6aEUw1gBLcKoB0,3273
@@ -38,7 +38,7 @@ posthoganalytics/test/test_before_send.py,sha256=A1_UVMewhHAvO39rZDWfS606vG_X-q0
38
38
  posthoganalytics/test/test_client.py,sha256=e1dD9bFplZWROiP35fuyBDguGXC6ZmG5j79Iw2t_NBw,96363
39
39
  posthoganalytics/test/test_consumer.py,sha256=HGMfU9PzQ5ZAe_R3kHnZNsMvD7jUjHL-gie0isrvMMk,7107
40
40
  posthoganalytics/test/test_contexts.py,sha256=c--hNUIEf6SHQ7H9vdPhU1oLCN0SnD4wDbFr-eLPHDo,7013
41
- posthoganalytics/test/test_exception_capture.py,sha256=al37Kg6wjzL_IBCFUUXRvkP6nVrqS6IZRCOKSo29Nh8,1063
41
+ posthoganalytics/test/test_exception_capture.py,sha256=i-979ZHfi1KGndhyEkKLRrYVtpUI7OfpE9U8VsNinmU,8993
42
42
  posthoganalytics/test/test_feature_flag.py,sha256=9RQwB5eCvVAGrrO7UnR3Z1OidP_YoL4iBl3A83fuAig,6824
43
43
  posthoganalytics/test/test_feature_flag_result.py,sha256=z2OgD97r85LKMqCnoCqAs74WjUMucayAtC3qWaITGCA,15898
44
44
  posthoganalytics/test/test_feature_flags.py,sha256=b0CcW2JyBRhOxlWX9KOBncqL7OG_VHFX3Z4J6VOlPNs,217352
@@ -47,8 +47,8 @@ posthoganalytics/test/test_request.py,sha256=Zc0VbkjpVmj8mKokQm9rzdgTr0b1U44vvMY
47
47
  posthoganalytics/test/test_size_limited_dict.py,sha256=-5IQjIEr_-Dql24M0HusdR_XroOMrtgiT0v6ZQCRvzo,774
48
48
  posthoganalytics/test/test_types.py,sha256=bRPHdwVpP7hu7emsplU8UVyzSQptv6PaG5lAoOD_BtM,7595
49
49
  posthoganalytics/test/test_utils.py,sha256=sqUTbfweVcxxFRd3WDMFXqPMyU6DvzOBeAOc68Py9aw,9620
50
- posthoganalytics-6.8.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
51
- posthoganalytics-6.8.0.dist-info/METADATA,sha256=aJLZEPJb-8QghbEJJ3GUEBEAcib56DsCiIs6-kFiwls,6024
52
- posthoganalytics-6.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- posthoganalytics-6.8.0.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
54
- posthoganalytics-6.8.0.dist-info/RECORD,,
50
+ posthoganalytics-6.9.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
51
+ posthoganalytics-6.9.0.dist-info/METADATA,sha256=eVZTY6z46Bgtxy3p_6YtoAGXX7Mot6YfEdEKouixpew,6024
52
+ posthoganalytics-6.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ posthoganalytics-6.9.0.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
54
+ posthoganalytics-6.9.0.dist-info/RECORD,,