posthog 6.8.0__tar.gz → 6.9.1__tar.gz

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 (62) hide show
  1. {posthog-6.8.0/posthog.egg-info → posthog-6.9.1}/PKG-INFO +1 -1
  2. {posthog-6.8.0 → posthog-6.9.1}/posthog/__init__.py +40 -2
  3. {posthog-6.8.0 → posthog-6.9.1}/posthog/client.py +50 -15
  4. {posthog-6.8.0 → posthog-6.9.1}/posthog/contexts.py +81 -0
  5. {posthog-6.8.0 → posthog-6.9.1}/posthog/exception_utils.py +192 -0
  6. posthog-6.9.1/posthog/test/test_exception_capture.py +334 -0
  7. {posthog-6.8.0 → posthog-6.9.1}/posthog/version.py +1 -1
  8. {posthog-6.8.0 → posthog-6.9.1/posthog.egg-info}/PKG-INFO +1 -1
  9. posthog-6.8.0/posthog/test/test_exception_capture.py +0 -34
  10. {posthog-6.8.0 → posthog-6.9.1}/LICENSE +0 -0
  11. {posthog-6.8.0 → posthog-6.9.1}/MANIFEST.in +0 -0
  12. {posthog-6.8.0 → posthog-6.9.1}/README.md +0 -0
  13. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/__init__.py +0 -0
  14. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/anthropic/__init__.py +0 -0
  15. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/anthropic/anthropic.py +0 -0
  16. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/anthropic/anthropic_async.py +0 -0
  17. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/anthropic/anthropic_converter.py +0 -0
  18. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/anthropic/anthropic_providers.py +0 -0
  19. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/gemini/__init__.py +0 -0
  20. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/gemini/gemini.py +0 -0
  21. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/gemini/gemini_converter.py +0 -0
  22. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/langchain/__init__.py +0 -0
  23. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/langchain/callbacks.py +0 -0
  24. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/openai/__init__.py +0 -0
  25. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/openai/openai.py +0 -0
  26. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/openai/openai_async.py +0 -0
  27. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/openai/openai_converter.py +0 -0
  28. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/openai/openai_providers.py +0 -0
  29. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/sanitization.py +0 -0
  30. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/types.py +0 -0
  31. {posthog-6.8.0 → posthog-6.9.1}/posthog/ai/utils.py +0 -0
  32. {posthog-6.8.0 → posthog-6.9.1}/posthog/args.py +0 -0
  33. {posthog-6.8.0 → posthog-6.9.1}/posthog/consumer.py +0 -0
  34. {posthog-6.8.0 → posthog-6.9.1}/posthog/exception_capture.py +0 -0
  35. {posthog-6.8.0 → posthog-6.9.1}/posthog/feature_flags.py +0 -0
  36. {posthog-6.8.0 → posthog-6.9.1}/posthog/integrations/__init__.py +0 -0
  37. {posthog-6.8.0 → posthog-6.9.1}/posthog/integrations/django.py +0 -0
  38. {posthog-6.8.0 → posthog-6.9.1}/posthog/poller.py +0 -0
  39. {posthog-6.8.0 → posthog-6.9.1}/posthog/py.typed +0 -0
  40. {posthog-6.8.0 → posthog-6.9.1}/posthog/request.py +0 -0
  41. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/__init__.py +0 -0
  42. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_before_send.py +0 -0
  43. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_client.py +0 -0
  44. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_consumer.py +0 -0
  45. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_contexts.py +0 -0
  46. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_feature_flag.py +0 -0
  47. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_feature_flag_result.py +0 -0
  48. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_feature_flags.py +0 -0
  49. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_module.py +0 -0
  50. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_request.py +0 -0
  51. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_size_limited_dict.py +0 -0
  52. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_types.py +0 -0
  53. {posthog-6.8.0 → posthog-6.9.1}/posthog/test/test_utils.py +0 -0
  54. {posthog-6.8.0 → posthog-6.9.1}/posthog/types.py +0 -0
  55. {posthog-6.8.0 → posthog-6.9.1}/posthog/utils.py +0 -0
  56. {posthog-6.8.0 → posthog-6.9.1}/posthog.egg-info/SOURCES.txt +0 -0
  57. {posthog-6.8.0 → posthog-6.9.1}/posthog.egg-info/dependency_links.txt +0 -0
  58. {posthog-6.8.0 → posthog-6.9.1}/posthog.egg-info/requires.txt +0 -0
  59. {posthog-6.8.0 → posthog-6.9.1}/posthog.egg-info/top_level.txt +0 -0
  60. {posthog-6.8.0 → posthog-6.9.1}/pyproject.toml +0 -0
  61. {posthog-6.8.0 → posthog-6.9.1}/setup.cfg +0 -0
  62. {posthog-6.8.0 → posthog-6.9.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: posthog
3
- Version: 6.8.0
3
+ Version: 6.9.1
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -10,6 +10,13 @@ from posthog.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,
16
+ )
17
+ from posthog.exception_utils import (
18
+ DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
19
+ DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
13
20
  )
14
21
  from posthog.feature_flags import InconclusiveMatchError, RequiresServerEvaluation
15
22
  from posthog.types import FeatureFlag, FlagsAndPayloads, FeatureFlagResult
@@ -20,13 +27,14 @@ __version__ = VERSION
20
27
  """Context management."""
21
28
 
22
29
 
23
- def new_context(fresh=False, capture_exceptions=True):
30
+ def new_context(fresh=False, capture_exceptions=True, client=None):
24
31
  """
25
32
  Create a new context scope that will be active for the duration of the with block.
26
33
 
27
34
  Args:
28
35
  fresh: Whether to start with a fresh context (default: False)
29
36
  capture_exceptions: Whether to capture exceptions raised within the context (default: True)
37
+ client: Optional Posthog client instance to use for this context (default: None)
30
38
 
31
39
  Examples:
32
40
  ```python
@@ -39,7 +47,9 @@ def new_context(fresh=False, capture_exceptions=True):
39
47
  Category:
40
48
  Contexts
41
49
  """
42
- return inner_new_context(fresh=fresh, capture_exceptions=capture_exceptions)
50
+ return inner_new_context(
51
+ fresh=fresh, capture_exceptions=capture_exceptions, client=client
52
+ )
43
53
 
44
54
 
45
55
  def scoped(fresh=False, capture_exceptions=True):
@@ -103,6 +113,27 @@ def identify_context(distinct_id: str):
103
113
  return inner_identify_context(distinct_id)
104
114
 
105
115
 
116
+ def set_capture_exception_code_variables_context(enabled: bool):
117
+ """
118
+ Set whether code variables are captured for the current context.
119
+ """
120
+ return inner_set_capture_exception_code_variables_context(enabled)
121
+
122
+
123
+ def set_code_variables_mask_patterns_context(mask_patterns: list):
124
+ """
125
+ Variable names matching these patterns will be masked with *** when capturing code variables.
126
+ """
127
+ return inner_set_code_variables_mask_patterns_context(mask_patterns)
128
+
129
+
130
+ def set_code_variables_ignore_patterns_context(ignore_patterns: list):
131
+ """
132
+ Variable names matching these patterns will be ignored completely when capturing code variables.
133
+ """
134
+ return inner_set_code_variables_ignore_patterns_context(ignore_patterns)
135
+
136
+
106
137
  def tag(name: str, value: Any):
107
138
  """
108
139
  Add a tag to the current context.
@@ -150,6 +181,10 @@ enable_local_evaluation = True # type: bool
150
181
 
151
182
  default_client = None # type: Optional[Client]
152
183
 
184
+ capture_exception_code_variables = False
185
+ code_variables_mask_patterns = DEFAULT_CODE_VARIABLES_MASK_PATTERNS
186
+ code_variables_ignore_patterns = DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS
187
+
153
188
 
154
189
  # NOTE - this and following functions take unpacked kwargs because we needed to make
155
190
  # it impossible to write `posthog.capture(distinct-id, event-name)` - basically, to enforce
@@ -744,6 +779,9 @@ def setup() -> Client:
744
779
  enable_exception_autocapture=enable_exception_autocapture,
745
780
  log_captured_exceptions=log_captured_exceptions,
746
781
  enable_local_evaluation=enable_local_evaluation,
782
+ capture_exception_code_variables=capture_exception_code_variables,
783
+ code_variables_mask_patterns=code_variables_mask_patterns,
784
+ code_variables_ignore_patterns=code_variables_ignore_patterns,
747
785
  )
748
786
 
749
787
  # always set incase user changes it
@@ -19,6 +19,9 @@ from posthog.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 posthog.feature_flags import (
24
27
  InconclusiveMatchError,
@@ -39,6 +42,9 @@ from posthog.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 posthog.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()
@@ -704,21 +725,7 @@ class Client(object):
704
725
  Examples:
705
726
  ```python
706
727
  # Set with distinct id
707
- posthog.capture(
708
- 'event_name',
709
- distinct_id='user-distinct-id',
710
- properties={
711
- '$set': {'name': 'Max Hedgehog'},
712
- '$set_once': {'initial_url': '/blog'}
713
- }
714
- )
715
- ```
716
- ```python
717
- # Set using context
718
- from posthog import new_context, identify_context
719
- with new_context():
720
- identify_context('user-distinct-id')
721
- posthog.capture('event_name')
728
+ posthog.set(distinct_id='user123', properties={'name': 'Max Hedgehog'})
722
729
  ```
723
730
 
724
731
  Category:
@@ -979,6 +986,34 @@ class Client(object):
979
986
  **properties,
980
987
  }
981
988
 
989
+ context_enabled = get_capture_exception_code_variables_context()
990
+ context_mask = get_code_variables_mask_patterns_context()
991
+ context_ignore = get_code_variables_ignore_patterns_context()
992
+
993
+ enabled = (
994
+ context_enabled
995
+ if context_enabled is not None
996
+ else self.capture_exception_code_variables
997
+ )
998
+ mask_patterns = (
999
+ context_mask
1000
+ if context_mask is not None
1001
+ else self.code_variables_mask_patterns
1002
+ )
1003
+ ignore_patterns = (
1004
+ context_ignore
1005
+ if context_ignore is not None
1006
+ else self.code_variables_ignore_patterns
1007
+ )
1008
+
1009
+ if enabled:
1010
+ try_attach_code_variables_to_frames(
1011
+ all_exceptions_with_trace_and_in_app,
1012
+ exc_info,
1013
+ mask_patterns=mask_patterns,
1014
+ ignore_patterns=ignore_patterns,
1015
+ )
1016
+
982
1017
  if self.log_captured_exceptions:
983
1018
  self.log.exception(exception, extra=kwargs)
984
1019
 
@@ -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 posthog.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
@@ -0,0 +1,334 @@
1
+ import subprocess
2
+ import sys
3
+ from textwrap import dedent
4
+
5
+ import pytest
6
+
7
+
8
+ def test_excepthook(tmpdir):
9
+ app = tmpdir.join("app.py")
10
+ app.write(
11
+ dedent(
12
+ """
13
+ from posthog import Posthog
14
+ posthog = Posthog('phc_x', host='https://eu.i.posthog.com', enable_exception_autocapture=True, debug=True, on_error=lambda e, batch: print('error handling batch: ', e, batch))
15
+
16
+ # frame_value = "LOL"
17
+
18
+ 1/0
19
+ """
20
+ )
21
+ )
22
+
23
+ with pytest.raises(subprocess.CalledProcessError) as excinfo:
24
+ subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
25
+
26
+ output = excinfo.value.output
27
+
28
+ assert b"ZeroDivisionError" in output
29
+ assert b"LOL" in output
30
+ assert b"DEBUG:posthog:data uploaded successfully" in output
31
+ assert (
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
+ in output
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
@@ -1,4 +1,4 @@
1
- VERSION = "6.8.0"
1
+ VERSION = "6.9.1"
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: posthog
3
- Version: 6.8.0
3
+ Version: 6.9.1
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -1,34 +0,0 @@
1
- import subprocess
2
- import sys
3
- from textwrap import dedent
4
-
5
- import pytest
6
-
7
-
8
- def test_excepthook(tmpdir):
9
- app = tmpdir.join("app.py")
10
- app.write(
11
- dedent(
12
- """
13
- from posthog import Posthog
14
- posthog = Posthog('phc_x', host='https://eu.i.posthog.com', enable_exception_autocapture=True, debug=True, on_error=lambda e, batch: print('error handling batch: ', e, batch))
15
-
16
- # frame_value = "LOL"
17
-
18
- 1/0
19
- """
20
- )
21
- )
22
-
23
- with pytest.raises(subprocess.CalledProcessError) as excinfo:
24
- subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
25
-
26
- output = excinfo.value.output
27
-
28
- assert b"ZeroDivisionError" in output
29
- assert b"LOL" in output
30
- assert b"DEBUG:posthog:data uploaded successfully" in output
31
- assert (
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
- in output
34
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes