posthog 6.7.2__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.
posthog/ai/utils.py CHANGED
@@ -1,10 +1,9 @@
1
1
  import time
2
2
  import uuid
3
- from typing import Any, Callable, Dict, Optional
4
-
3
+ from typing import Any, Callable, Dict, List, Optional, cast
5
4
 
6
5
  from posthog.client import Client as PostHogClient
7
- from posthog.ai.types import StreamingEventData, StreamingUsageStats
6
+ from posthog.ai.types import FormattedMessage, StreamingEventData, TokenUsage
8
7
  from posthog.ai.sanitization import (
9
8
  sanitize_openai,
10
9
  sanitize_anthropic,
@@ -14,7 +13,7 @@ from posthog.ai.sanitization import (
14
13
 
15
14
 
16
15
  def merge_usage_stats(
17
- target: Dict[str, int], source: StreamingUsageStats, mode: str = "incremental"
16
+ target: TokenUsage, source: TokenUsage, mode: str = "incremental"
18
17
  ) -> None:
19
18
  """
20
19
  Merge streaming usage statistics into target dict, handling None values.
@@ -25,19 +24,58 @@ def merge_usage_stats(
25
24
 
26
25
  Args:
27
26
  target: Dictionary to update with usage stats
28
- source: StreamingUsageStats that may contain None values
27
+ source: TokenUsage that may contain None values
29
28
  mode: Either "incremental" or "cumulative"
30
29
  """
31
30
  if mode == "incremental":
32
31
  # Add new values to existing totals
33
- for key, value in source.items():
34
- if value is not None and isinstance(value, int):
35
- target[key] = target.get(key, 0) + value
32
+ source_input = source.get("input_tokens")
33
+ if source_input is not None:
34
+ current = target.get("input_tokens") or 0
35
+ target["input_tokens"] = current + source_input
36
+
37
+ source_output = source.get("output_tokens")
38
+ if source_output is not None:
39
+ current = target.get("output_tokens") or 0
40
+ target["output_tokens"] = current + source_output
41
+
42
+ source_cache_read = source.get("cache_read_input_tokens")
43
+ if source_cache_read is not None:
44
+ current = target.get("cache_read_input_tokens") or 0
45
+ target["cache_read_input_tokens"] = current + source_cache_read
46
+
47
+ source_cache_creation = source.get("cache_creation_input_tokens")
48
+ if source_cache_creation is not None:
49
+ current = target.get("cache_creation_input_tokens") or 0
50
+ target["cache_creation_input_tokens"] = current + source_cache_creation
51
+
52
+ source_reasoning = source.get("reasoning_tokens")
53
+ if source_reasoning is not None:
54
+ current = target.get("reasoning_tokens") or 0
55
+ target["reasoning_tokens"] = current + source_reasoning
56
+
57
+ source_web_search = source.get("web_search_count")
58
+ if source_web_search is not None:
59
+ current = target.get("web_search_count") or 0
60
+ target["web_search_count"] = max(current, source_web_search)
61
+
36
62
  elif mode == "cumulative":
37
63
  # Replace with latest values (already cumulative)
38
- for key, value in source.items():
39
- if value is not None and isinstance(value, int):
40
- target[key] = value
64
+ if source.get("input_tokens") is not None:
65
+ target["input_tokens"] = source["input_tokens"]
66
+ if source.get("output_tokens") is not None:
67
+ target["output_tokens"] = source["output_tokens"]
68
+ if source.get("cache_read_input_tokens") is not None:
69
+ target["cache_read_input_tokens"] = source["cache_read_input_tokens"]
70
+ if source.get("cache_creation_input_tokens") is not None:
71
+ target["cache_creation_input_tokens"] = source[
72
+ "cache_creation_input_tokens"
73
+ ]
74
+ if source.get("reasoning_tokens") is not None:
75
+ target["reasoning_tokens"] = source["reasoning_tokens"]
76
+ if source.get("web_search_count") is not None:
77
+ target["web_search_count"] = source["web_search_count"]
78
+
41
79
  else:
42
80
  raise ValueError(f"Invalid mode: {mode}. Must be 'incremental' or 'cumulative'")
43
81
 
@@ -64,74 +102,31 @@ def get_model_params(kwargs: Dict[str, Any]) -> Dict[str, Any]:
64
102
  return model_params
65
103
 
66
104
 
67
- def get_usage(response, provider: str) -> Dict[str, Any]:
105
+ def get_usage(response, provider: str) -> TokenUsage:
106
+ """
107
+ Extract usage statistics from response based on provider.
108
+ Delegates to provider-specific converter functions.
109
+ """
68
110
  if provider == "anthropic":
69
- return {
70
- "input_tokens": response.usage.input_tokens,
71
- "output_tokens": response.usage.output_tokens,
72
- "cache_read_input_tokens": response.usage.cache_read_input_tokens,
73
- "cache_creation_input_tokens": response.usage.cache_creation_input_tokens,
74
- }
111
+ from posthog.ai.anthropic.anthropic_converter import (
112
+ extract_anthropic_usage_from_response,
113
+ )
114
+
115
+ return extract_anthropic_usage_from_response(response)
75
116
  elif provider == "openai":
76
- cached_tokens = 0
77
- input_tokens = 0
78
- output_tokens = 0
79
- reasoning_tokens = 0
80
-
81
- # responses api
82
- if hasattr(response.usage, "input_tokens"):
83
- input_tokens = response.usage.input_tokens
84
- if hasattr(response.usage, "output_tokens"):
85
- output_tokens = response.usage.output_tokens
86
- if hasattr(response.usage, "input_tokens_details") and hasattr(
87
- response.usage.input_tokens_details, "cached_tokens"
88
- ):
89
- cached_tokens = response.usage.input_tokens_details.cached_tokens
90
- if hasattr(response.usage, "output_tokens_details") and hasattr(
91
- response.usage.output_tokens_details, "reasoning_tokens"
92
- ):
93
- reasoning_tokens = response.usage.output_tokens_details.reasoning_tokens
94
-
95
- # chat completions
96
- if hasattr(response.usage, "prompt_tokens"):
97
- input_tokens = response.usage.prompt_tokens
98
- if hasattr(response.usage, "completion_tokens"):
99
- output_tokens = response.usage.completion_tokens
100
- if hasattr(response.usage, "prompt_tokens_details") and hasattr(
101
- response.usage.prompt_tokens_details, "cached_tokens"
102
- ):
103
- cached_tokens = response.usage.prompt_tokens_details.cached_tokens
117
+ from posthog.ai.openai.openai_converter import (
118
+ extract_openai_usage_from_response,
119
+ )
104
120
 
105
- return {
106
- "input_tokens": input_tokens,
107
- "output_tokens": output_tokens,
108
- "cache_read_input_tokens": cached_tokens,
109
- "reasoning_tokens": reasoning_tokens,
110
- }
121
+ return extract_openai_usage_from_response(response)
111
122
  elif provider == "gemini":
112
- input_tokens = 0
113
- output_tokens = 0
123
+ from posthog.ai.gemini.gemini_converter import (
124
+ extract_gemini_usage_from_response,
125
+ )
114
126
 
115
- if hasattr(response, "usage_metadata") and response.usage_metadata:
116
- input_tokens = getattr(response.usage_metadata, "prompt_token_count", 0)
117
- output_tokens = getattr(
118
- response.usage_metadata, "candidates_token_count", 0
119
- )
127
+ return extract_gemini_usage_from_response(response)
120
128
 
121
- return {
122
- "input_tokens": input_tokens,
123
- "output_tokens": output_tokens,
124
- "cache_read_input_tokens": 0,
125
- "cache_creation_input_tokens": 0,
126
- "reasoning_tokens": 0,
127
- }
128
- return {
129
- "input_tokens": 0,
130
- "output_tokens": 0,
131
- "cache_read_input_tokens": 0,
132
- "cache_creation_input_tokens": 0,
133
- "reasoning_tokens": 0,
134
- }
129
+ return TokenUsage(input_tokens=0, output_tokens=0)
135
130
 
136
131
 
137
132
  def format_response(response, provider: str):
@@ -169,9 +164,12 @@ def extract_available_tool_calls(provider: str, kwargs: Dict[str, Any]):
169
164
  from posthog.ai.openai.openai_converter import extract_openai_tools
170
165
 
171
166
  return extract_openai_tools(kwargs)
167
+ return None
172
168
 
173
169
 
174
- def merge_system_prompt(kwargs: Dict[str, Any], provider: str):
170
+ def merge_system_prompt(
171
+ kwargs: Dict[str, Any], provider: str
172
+ ) -> List[FormattedMessage]:
175
173
  """
176
174
  Merge system prompts and format messages for the given provider.
177
175
  """
@@ -182,14 +180,15 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str):
182
180
  system = kwargs.get("system")
183
181
  return format_anthropic_input(messages, system)
184
182
  elif provider == "gemini":
185
- from posthog.ai.gemini.gemini_converter import format_gemini_input
183
+ from posthog.ai.gemini.gemini_converter import format_gemini_input_with_system
186
184
 
187
185
  contents = kwargs.get("contents", [])
188
- return format_gemini_input(contents)
186
+ config = kwargs.get("config")
187
+ return format_gemini_input_with_system(contents, config)
189
188
  elif provider == "openai":
190
- # For OpenAI, handle both Chat Completions and Responses API
191
189
  from posthog.ai.openai.openai_converter import format_openai_input
192
190
 
191
+ # For OpenAI, handle both Chat Completions and Responses API
193
192
  messages_param = kwargs.get("messages")
194
193
  input_param = kwargs.get("input")
195
194
 
@@ -200,9 +199,11 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str):
200
199
  if kwargs.get("system") is not None:
201
200
  has_system = any(msg.get("role") == "system" for msg in messages)
202
201
  if not has_system:
203
- messages = [
204
- {"role": "system", "content": kwargs.get("system")}
205
- ] + messages
202
+ system_msg = cast(
203
+ FormattedMessage,
204
+ {"role": "system", "content": kwargs.get("system")},
205
+ )
206
+ messages = [system_msg] + messages
206
207
 
207
208
  # For Responses API, add instructions to the system prompt if provided
208
209
  if kwargs.get("instructions") is not None:
@@ -220,9 +221,11 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str):
220
221
  )
221
222
  else:
222
223
  # Create a new system message with instructions
223
- messages = [
224
- {"role": "system", "content": kwargs.get("instructions")}
225
- ] + messages
224
+ instruction_msg = cast(
225
+ FormattedMessage,
226
+ {"role": "system", "content": kwargs.get("instructions")},
227
+ )
228
+ messages = [instruction_msg] + messages
226
229
 
227
230
  return messages
228
231
 
@@ -250,7 +253,7 @@ def call_llm_and_track_usage(
250
253
  response = None
251
254
  error = None
252
255
  http_status = 200
253
- usage: Dict[str, Any] = {}
256
+ usage: TokenUsage = TokenUsage()
254
257
  error_params: Dict[str, Any] = {}
255
258
 
256
259
  try:
@@ -305,27 +308,21 @@ def call_llm_and_track_usage(
305
308
  if available_tool_calls:
306
309
  event_properties["$ai_tools"] = available_tool_calls
307
310
 
308
- if (
309
- usage.get("cache_read_input_tokens") is not None
310
- and usage.get("cache_read_input_tokens", 0) > 0
311
- ):
312
- event_properties["$ai_cache_read_input_tokens"] = usage.get(
313
- "cache_read_input_tokens", 0
314
- )
311
+ cache_read = usage.get("cache_read_input_tokens")
312
+ if cache_read is not None and cache_read > 0:
313
+ event_properties["$ai_cache_read_input_tokens"] = cache_read
315
314
 
316
- if (
317
- usage.get("cache_creation_input_tokens") is not None
318
- and usage.get("cache_creation_input_tokens", 0) > 0
319
- ):
320
- event_properties["$ai_cache_creation_input_tokens"] = usage.get(
321
- "cache_creation_input_tokens", 0
322
- )
315
+ cache_creation = usage.get("cache_creation_input_tokens")
316
+ if cache_creation is not None and cache_creation > 0:
317
+ event_properties["$ai_cache_creation_input_tokens"] = cache_creation
323
318
 
324
- if (
325
- usage.get("reasoning_tokens") is not None
326
- and usage.get("reasoning_tokens", 0) > 0
327
- ):
328
- event_properties["$ai_reasoning_tokens"] = usage.get("reasoning_tokens", 0)
319
+ reasoning = usage.get("reasoning_tokens")
320
+ if reasoning is not None and reasoning > 0:
321
+ event_properties["$ai_reasoning_tokens"] = reasoning
322
+
323
+ web_search_count = usage.get("web_search_count")
324
+ if web_search_count is not None and web_search_count > 0:
325
+ event_properties["$ai_web_search_count"] = web_search_count
329
326
 
330
327
  if posthog_distinct_id is None:
331
328
  event_properties["$process_person_profile"] = False
@@ -367,7 +364,7 @@ async def call_llm_and_track_usage_async(
367
364
  response = None
368
365
  error = None
369
366
  http_status = 200
370
- usage: Dict[str, Any] = {}
367
+ usage: TokenUsage = TokenUsage()
371
368
  error_params: Dict[str, Any] = {}
372
369
 
373
370
  try:
@@ -422,21 +419,21 @@ async def call_llm_and_track_usage_async(
422
419
  if available_tool_calls:
423
420
  event_properties["$ai_tools"] = available_tool_calls
424
421
 
425
- if (
426
- usage.get("cache_read_input_tokens") is not None
427
- and usage.get("cache_read_input_tokens", 0) > 0
428
- ):
429
- event_properties["$ai_cache_read_input_tokens"] = usage.get(
430
- "cache_read_input_tokens", 0
431
- )
422
+ cache_read = usage.get("cache_read_input_tokens")
423
+ if cache_read is not None and cache_read > 0:
424
+ event_properties["$ai_cache_read_input_tokens"] = cache_read
432
425
 
433
- if (
434
- usage.get("cache_creation_input_tokens") is not None
435
- and usage.get("cache_creation_input_tokens", 0) > 0
436
- ):
437
- event_properties["$ai_cache_creation_input_tokens"] = usage.get(
438
- "cache_creation_input_tokens", 0
439
- )
426
+ cache_creation = usage.get("cache_creation_input_tokens")
427
+ if cache_creation is not None and cache_creation > 0:
428
+ event_properties["$ai_cache_creation_input_tokens"] = cache_creation
429
+
430
+ reasoning = usage.get("reasoning_tokens")
431
+ if reasoning is not None and reasoning > 0:
432
+ event_properties["$ai_reasoning_tokens"] = reasoning
433
+
434
+ web_search_count = usage.get("web_search_count")
435
+ if web_search_count is not None and web_search_count > 0:
436
+ event_properties["$ai_web_search_count"] = web_search_count
440
437
 
441
438
  if posthog_distinct_id is None:
442
439
  event_properties["$process_person_profile"] = False
@@ -559,6 +556,15 @@ def capture_streaming_event(
559
556
  if value is not None and isinstance(value, int) and value > 0:
560
557
  event_properties[f"$ai_{field}"] = value
561
558
 
559
+ # Add web search count if present (all providers)
560
+ web_search_count = event_data["usage_stats"].get("web_search_count")
561
+ if (
562
+ web_search_count is not None
563
+ and isinstance(web_search_count, int)
564
+ and web_search_count > 0
565
+ ):
566
+ event_properties["$ai_web_search_count"] = web_search_count
567
+
562
568
  # Handle provider-specific fields
563
569
  if (
564
570
  event_data["provider"] == "openai"
posthog/client.py CHANGED
@@ -19,8 +19,15 @@ 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,
25
+ )
26
+ from posthog.feature_flags import (
27
+ InconclusiveMatchError,
28
+ RequiresServerEvaluation,
29
+ match_feature_flag_properties,
22
30
  )
23
- from posthog.feature_flags import InconclusiveMatchError, match_feature_flag_properties
24
31
  from posthog.poller import Poller
25
32
  from posthog.request import (
26
33
  DEFAULT_HOST,
@@ -35,6 +42,9 @@ from posthog.contexts import (
35
42
  _get_current_context,
36
43
  get_context_distinct_id,
37
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,
38
48
  new_context,
39
49
  )
40
50
  from posthog.types import (
@@ -56,7 +66,6 @@ from posthog.utils import (
56
66
  SizeLimitedDict,
57
67
  clean,
58
68
  guess_timezone,
59
- remove_trailing_slash,
60
69
  system_context,
61
70
  )
62
71
  from posthog.version import VERSION
@@ -99,6 +108,34 @@ def add_context_tags(properties):
99
108
  return properties
100
109
 
101
110
 
111
+ def no_throw(default_return=None):
112
+ """
113
+ Decorator to prevent raising exceptions from public API methods.
114
+ Note that this doesn't prevent errors from propagating via `on_error`.
115
+ Exceptions will still be raised if the debug flag is enabled.
116
+
117
+ Args:
118
+ default_return: Value to return on exception (default: None)
119
+ """
120
+
121
+ def decorator(func):
122
+ from functools import wraps
123
+
124
+ @wraps(func)
125
+ def wrapper(self, *args, **kwargs):
126
+ try:
127
+ return func(self, *args, **kwargs)
128
+ except Exception as e:
129
+ if self.debug:
130
+ raise e
131
+ self.log.exception(f"Error in {func.__name__}: {e}")
132
+ return default_return
133
+
134
+ return wrapper
135
+
136
+ return decorator
137
+
138
+
102
139
  class Client(object):
103
140
  """
104
141
  This is the SDK reference for the PostHog Python SDK.
@@ -147,6 +184,9 @@ class Client(object):
147
184
  before_send=None,
148
185
  flag_fallback_cache_url=None,
149
186
  enable_local_evaluation=True,
187
+ capture_exception_code_variables=False,
188
+ code_variables_mask_patterns=None,
189
+ code_variables_ignore_patterns=None,
150
190
  ):
151
191
  """
152
192
  Initialize a new PostHog client instance.
@@ -202,6 +242,18 @@ class Client(object):
202
242
  self.privacy_mode = privacy_mode
203
243
  self.enable_local_evaluation = enable_local_evaluation
204
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
+
205
257
  if project_root is None:
206
258
  try:
207
259
  project_root = os.getcwd()
@@ -481,6 +533,7 @@ class Client(object):
481
533
 
482
534
  return normalize_flags_response(resp_data)
483
535
 
536
+ @no_throw()
484
537
  def capture(
485
538
  self, event: str, **kwargs: Unpack[OptionalCaptureArgs]
486
539
  ) -> Optional[str]:
@@ -657,6 +710,7 @@ class Client(object):
657
710
  f"Expected bool or dict."
658
711
  )
659
712
 
713
+ @no_throw()
660
714
  def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
661
715
  """
662
716
  Set properties on a person profile.
@@ -690,6 +744,8 @@ class Client(object):
690
744
 
691
745
  Category:
692
746
  Identification
747
+
748
+ Note: This method will not raise exceptions. Errors are logged.
693
749
  """
694
750
  distinct_id = kwargs.get("distinct_id", None)
695
751
  properties = kwargs.get("properties", None)
@@ -716,6 +772,7 @@ class Client(object):
716
772
 
717
773
  return self._enqueue(msg, disable_geoip)
718
774
 
775
+ @no_throw()
719
776
  def set_once(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
720
777
  """
721
778
  Set properties on a person profile only if they haven't been set before.
@@ -734,6 +791,8 @@ class Client(object):
734
791
 
735
792
  Category:
736
793
  Identification
794
+
795
+ Note: This method will not raise exceptions. Errors are logged.
737
796
  """
738
797
  distinct_id = kwargs.get("distinct_id", None)
739
798
  properties = kwargs.get("properties", None)
@@ -759,6 +818,7 @@ class Client(object):
759
818
 
760
819
  return self._enqueue(msg, disable_geoip)
761
820
 
821
+ @no_throw()
762
822
  def group_identify(
763
823
  self,
764
824
  group_type: str,
@@ -791,6 +851,8 @@ class Client(object):
791
851
 
792
852
  Category:
793
853
  Identification
854
+
855
+ Note: This method will not raise exceptions. Errors are logged.
794
856
  """
795
857
  properties = properties or {}
796
858
 
@@ -815,6 +877,7 @@ class Client(object):
815
877
 
816
878
  return self._enqueue(msg, disable_geoip)
817
879
 
880
+ @no_throw()
818
881
  def alias(
819
882
  self,
820
883
  previous_id: str,
@@ -840,6 +903,8 @@ class Client(object):
840
903
 
841
904
  Category:
842
905
  Identification
906
+
907
+ Note: This method will not raise exceptions. Errors are logged.
843
908
  """
844
909
  (distinct_id, personless) = get_identity_state(distinct_id)
845
910
 
@@ -932,10 +997,37 @@ class Client(object):
932
997
  "value"
933
998
  ),
934
999
  "$exception_list": all_exceptions_with_trace_and_in_app,
935
- "$exception_personURL": f"{remove_trailing_slash(self.raw_host)}/project/{self.api_key}/person/{distinct_id}",
936
1000
  **properties,
937
1001
  }
938
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
+
939
1031
  if self.log_captured_exceptions:
940
1032
  self.log.exception(exception, extra=kwargs)
941
1033
 
@@ -1543,7 +1635,7 @@ class Client(object):
1543
1635
  self.log.debug(
1544
1636
  f"Successfully computed flag locally: {key} -> {response}"
1545
1637
  )
1546
- except InconclusiveMatchError as e:
1638
+ except (RequiresServerEvaluation, InconclusiveMatchError) as e:
1547
1639
  self.log.debug(f"Failed to compute flag {key} locally: {e}")
1548
1640
  except Exception as e:
1549
1641
  self.log.exception(
posthog/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