posthog 7.0.1__py3-none-any.whl → 7.4.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.
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import re
2
3
  from typing import Any
3
4
  from urllib.parse import urlparse
@@ -5,6 +6,15 @@ from urllib.parse import urlparse
5
6
  REDACTED_IMAGE_PLACEHOLDER = "[base64 image redacted]"
6
7
 
7
8
 
9
+ def _is_multimodal_enabled() -> bool:
10
+ """Check if multimodal capture is enabled via environment variable."""
11
+ return os.environ.get("_INTERNAL_LLMA_MULTIMODAL", "").lower() in (
12
+ "true",
13
+ "1",
14
+ "yes",
15
+ )
16
+
17
+
8
18
  def is_base64_data_url(text: str) -> bool:
9
19
  return re.match(r"^data:([^;]+);base64,", text) is not None
10
20
 
@@ -27,6 +37,9 @@ def is_raw_base64(text: str) -> bool:
27
37
 
28
38
 
29
39
  def redact_base64_data_url(value: Any) -> Any:
40
+ if _is_multimodal_enabled():
41
+ return value
42
+
30
43
  if not isinstance(value, str):
31
44
  return value
32
45
 
@@ -83,6 +96,11 @@ def sanitize_openai_image(item: Any) -> Any:
83
96
  },
84
97
  }
85
98
 
99
+ if item.get("type") == "audio" and "data" in item:
100
+ if _is_multimodal_enabled():
101
+ return item
102
+ return {**item, "data": REDACTED_IMAGE_PLACEHOLDER}
103
+
86
104
  return item
87
105
 
88
106
 
@@ -100,6 +118,9 @@ def sanitize_openai_response_image(item: Any) -> Any:
100
118
 
101
119
 
102
120
  def sanitize_anthropic_image(item: Any) -> Any:
121
+ if _is_multimodal_enabled():
122
+ return item
123
+
103
124
  if not isinstance(item, dict):
104
125
  return item
105
126
 
@@ -109,8 +130,6 @@ def sanitize_anthropic_image(item: Any) -> Any:
109
130
  and item["source"].get("type") == "base64"
110
131
  and "data" in item["source"]
111
132
  ):
112
- # For Anthropic, if the source type is "base64", we should always redact the data
113
- # The provider is explicitly telling us this is base64 data
114
133
  return {
115
134
  **item,
116
135
  "source": {
@@ -123,6 +142,9 @@ def sanitize_anthropic_image(item: Any) -> Any:
123
142
 
124
143
 
125
144
  def sanitize_gemini_part(part: Any) -> Any:
145
+ if _is_multimodal_enabled():
146
+ return part
147
+
126
148
  if not isinstance(part, dict):
127
149
  return part
128
150
 
@@ -131,8 +153,6 @@ def sanitize_gemini_part(part: Any) -> Any:
131
153
  and isinstance(part["inline_data"], dict)
132
154
  and "data" in part["inline_data"]
133
155
  ):
134
- # For Gemini, the inline_data structure indicates base64 data
135
- # We should redact any string data in this context
136
156
  return {
137
157
  **part,
138
158
  "inline_data": {
@@ -185,7 +205,9 @@ def sanitize_langchain_image(item: Any) -> Any:
185
205
  and isinstance(item.get("source"), dict)
186
206
  and "data" in item["source"]
187
207
  ):
188
- # Anthropic style - raw base64 in structured format, always redact
208
+ if _is_multimodal_enabled():
209
+ return item
210
+
189
211
  return {
190
212
  **item,
191
213
  "source": {
posthog/client.py CHANGED
@@ -28,10 +28,17 @@ from posthog.feature_flags import (
28
28
  RequiresServerEvaluation,
29
29
  match_feature_flag_properties,
30
30
  )
31
+ from posthog.flag_definition_cache import (
32
+ FlagDefinitionCacheData,
33
+ FlagDefinitionCacheProvider,
34
+ )
31
35
  from posthog.poller import Poller
32
36
  from posthog.request import (
33
37
  DEFAULT_HOST,
34
38
  APIError,
39
+ QuotaLimitError,
40
+ RequestsConnectionError,
41
+ RequestsTimeout,
35
42
  batch_post,
36
43
  determine_server_host,
37
44
  flags,
@@ -49,6 +56,7 @@ from posthog.contexts import (
49
56
  )
50
57
  from posthog.types import (
51
58
  FeatureFlag,
59
+ FeatureFlagError,
52
60
  FeatureFlagResult,
53
61
  FlagMetadata,
54
62
  FlagsAndPayloads,
@@ -184,6 +192,7 @@ class Client(object):
184
192
  before_send=None,
185
193
  flag_fallback_cache_url=None,
186
194
  enable_local_evaluation=True,
195
+ flag_definition_cache_provider: Optional[FlagDefinitionCacheProvider] = None,
187
196
  capture_exception_code_variables=False,
188
197
  code_variables_mask_patterns=None,
189
198
  code_variables_ignore_patterns=None,
@@ -222,8 +231,8 @@ class Client(object):
222
231
  self.timeout = timeout
223
232
  self._feature_flags = None # private variable to store flags
224
233
  self.feature_flags_by_key = None
225
- self.group_type_mapping = None
226
- self.cohorts = None
234
+ self.group_type_mapping: Optional[dict[str, str]] = None
235
+ self.cohorts: Optional[dict[str, Any]] = None
227
236
  self.poll_interval = poll_interval
228
237
  self.feature_flags_request_timeout_seconds = (
229
238
  feature_flags_request_timeout_seconds
@@ -232,6 +241,8 @@ class Client(object):
232
241
  self.distinct_ids_feature_flags_reported = SizeLimitedDict(MAX_DICT_SIZE, set)
233
242
  self.flag_cache = self._initialize_flag_cache(flag_fallback_cache_url)
234
243
  self.flag_definition_version = 0
244
+ self._flags_etag: Optional[str] = None
245
+ self._flag_definition_cache_provider = flag_definition_cache_provider
235
246
  self.disabled = disabled
236
247
  self.disable_geoip = disable_geoip
237
248
  self.historical_migration = historical_migration
@@ -622,7 +633,28 @@ class Client(object):
622
633
  if flag_options["should_send"]:
623
634
  try:
624
635
  if flag_options["only_evaluate_locally"] is True:
625
- # Only use local evaluation
636
+ # Local evaluation explicitly requested
637
+ feature_variants = self.get_all_flags(
638
+ distinct_id,
639
+ groups=(groups or {}),
640
+ person_properties=flag_options["person_properties"],
641
+ group_properties=flag_options["group_properties"],
642
+ disable_geoip=disable_geoip,
643
+ only_evaluate_locally=True,
644
+ flag_keys_to_evaluate=flag_options["flag_keys_filter"],
645
+ )
646
+ elif flag_options["only_evaluate_locally"] is False:
647
+ # Remote evaluation explicitly requested
648
+ feature_variants = self.get_feature_variants(
649
+ distinct_id,
650
+ groups,
651
+ person_properties=flag_options["person_properties"],
652
+ group_properties=flag_options["group_properties"],
653
+ disable_geoip=disable_geoip,
654
+ flag_keys_to_evaluate=flag_options["flag_keys_filter"],
655
+ )
656
+ elif self.feature_flags:
657
+ # Local flags available, prefer local evaluation
626
658
  feature_variants = self.get_all_flags(
627
659
  distinct_id,
628
660
  groups=(groups or {}),
@@ -633,7 +665,7 @@ class Client(object):
633
665
  flag_keys_to_evaluate=flag_options["flag_keys_filter"],
634
666
  )
635
667
  else:
636
- # Default behavior - use remote evaluation
668
+ # Fall back to remote evaluation
637
669
  feature_variants = self.get_feature_variants(
638
670
  distinct_id,
639
671
  groups,
@@ -979,10 +1011,6 @@ class Client(object):
979
1011
  all_exceptions_with_trace_and_in_app = event["exception"]["values"]
980
1012
 
981
1013
  properties = {
982
- "$exception_type": all_exceptions_with_trace_and_in_app[0].get("type"),
983
- "$exception_message": all_exceptions_with_trace_and_in_app[0].get(
984
- "value"
985
- ),
986
1014
  "$exception_list": all_exceptions_with_trace_and_in_app,
987
1015
  **properties,
988
1016
  }
@@ -1147,17 +1175,25 @@ class Client(object):
1147
1175
  posthog.join()
1148
1176
  ```
1149
1177
  """
1150
- for consumer in self.consumers:
1151
- consumer.pause()
1152
- try:
1153
- consumer.join()
1154
- except RuntimeError:
1155
- # consumer thread has not started
1156
- pass
1178
+ if self.consumers:
1179
+ for consumer in self.consumers:
1180
+ consumer.pause()
1181
+ try:
1182
+ consumer.join()
1183
+ except RuntimeError:
1184
+ # consumer thread has not started
1185
+ pass
1157
1186
 
1158
1187
  if self.poller:
1159
1188
  self.poller.stop()
1160
1189
 
1190
+ # Shutdown the cache provider (release locks, cleanup)
1191
+ if self._flag_definition_cache_provider:
1192
+ try:
1193
+ self._flag_definition_cache_provider.shutdown()
1194
+ except Exception as e:
1195
+ self.log.error(f"[FEATURE FLAGS] Cache provider shutdown error: {e}")
1196
+
1161
1197
  def shutdown(self):
1162
1198
  """
1163
1199
  Flush all messages and cleanly shutdown the client. Call this before the process ends in serverless environments to avoid data loss.
@@ -1173,7 +1209,71 @@ class Client(object):
1173
1209
  if self.exception_capture:
1174
1210
  self.exception_capture.close()
1175
1211
 
1212
+ def _update_flag_state(
1213
+ self, data: FlagDefinitionCacheData, old_flags_by_key: Optional[dict] = None
1214
+ ) -> None:
1215
+ """Update internal flag state from cache data and invalidate evaluation cache if changed."""
1216
+ self.feature_flags = data["flags"]
1217
+ self.group_type_mapping = data["group_type_mapping"]
1218
+ self.cohorts = data["cohorts"]
1219
+
1220
+ # Invalidate evaluation cache if flag definitions changed
1221
+ if (
1222
+ self.flag_cache
1223
+ and old_flags_by_key is not None
1224
+ and old_flags_by_key != (self.feature_flags_by_key or {})
1225
+ ):
1226
+ old_version = self.flag_definition_version
1227
+ self.flag_definition_version += 1
1228
+ self.flag_cache.invalidate_version(old_version)
1229
+
1176
1230
  def _load_feature_flags(self):
1231
+ should_fetch = True
1232
+ if self._flag_definition_cache_provider:
1233
+ try:
1234
+ should_fetch = (
1235
+ self._flag_definition_cache_provider.should_fetch_flag_definitions()
1236
+ )
1237
+ except Exception as e:
1238
+ self.log.error(
1239
+ f"[FEATURE FLAGS] Cache provider should_fetch error: {e}"
1240
+ )
1241
+ # Fail-safe: fetch from API if cache provider errors
1242
+ should_fetch = True
1243
+
1244
+ # If not fetching, try to get from cache
1245
+ if not should_fetch and self._flag_definition_cache_provider:
1246
+ try:
1247
+ cached_data = (
1248
+ self._flag_definition_cache_provider.get_flag_definitions()
1249
+ )
1250
+ if cached_data:
1251
+ self.log.debug(
1252
+ "[FEATURE FLAGS] Using cached flag definitions from external cache"
1253
+ )
1254
+ self._update_flag_state(
1255
+ cached_data, old_flags_by_key=self.feature_flags_by_key or {}
1256
+ )
1257
+ self._last_feature_flag_poll = datetime.now(tz=tzutc())
1258
+ return
1259
+ else:
1260
+ # Emergency fallback: if cache is empty and we have no flags, fetch anyway.
1261
+ # There's really no other way of recovering in this case.
1262
+ if not self.feature_flags:
1263
+ self.log.debug(
1264
+ "[FEATURE FLAGS] Cache empty and no flags loaded, falling back to API fetch"
1265
+ )
1266
+ should_fetch = True
1267
+ except Exception as e:
1268
+ self.log.error(f"[FEATURE FLAGS] Cache provider get error: {e}")
1269
+ # Fail-safe: fetch from API if cache provider errors
1270
+ should_fetch = True
1271
+
1272
+ if should_fetch:
1273
+ self._fetch_feature_flags_from_api()
1274
+
1275
+ def _fetch_feature_flags_from_api(self):
1276
+ """Fetch feature flags from the PostHog API."""
1177
1277
  try:
1178
1278
  # Store old flags to detect changes
1179
1279
  old_flags_by_key: dict[str, dict] = self.feature_flags_by_key or {}
@@ -1183,19 +1283,41 @@ class Client(object):
1183
1283
  f"/api/feature_flag/local_evaluation/?token={self.api_key}&send_cohorts",
1184
1284
  self.host,
1185
1285
  timeout=10,
1286
+ etag=self._flags_etag,
1186
1287
  )
1187
1288
 
1188
- self.feature_flags = response["flags"] or []
1189
- self.group_type_mapping = response["group_type_mapping"] or {}
1190
- self.cohorts = response["cohorts"] or {}
1289
+ # Update stored ETag (clear if server stops sending one)
1290
+ self._flags_etag = response.etag
1291
+
1292
+ # If 304 Not Modified, flags haven't changed - skip processing
1293
+ if response.not_modified:
1294
+ self.log.debug(
1295
+ "[FEATURE FLAGS] Flags not modified (304), using cached data"
1296
+ )
1297
+ self._last_feature_flag_poll = datetime.now(tz=tzutc())
1298
+ return
1299
+
1300
+ if response.data is None:
1301
+ self.log.error(
1302
+ "[FEATURE FLAGS] Unexpected empty response data in non-304 response"
1303
+ )
1304
+ return
1305
+
1306
+ self._update_flag_state(response.data, old_flags_by_key=old_flags_by_key)
1191
1307
 
1192
- # Check if flag definitions changed and update version
1193
- if self.flag_cache and old_flags_by_key != (
1194
- self.feature_flags_by_key or {}
1195
- ):
1196
- old_version = self.flag_definition_version
1197
- self.flag_definition_version += 1
1198
- self.flag_cache.invalidate_version(old_version)
1308
+ # Store in external cache if provider is configured
1309
+ if self._flag_definition_cache_provider:
1310
+ try:
1311
+ self._flag_definition_cache_provider.on_flag_definitions_received(
1312
+ {
1313
+ "flags": self.feature_flags or [],
1314
+ "group_type_mapping": self.group_type_mapping or {},
1315
+ "cohorts": self.cohorts or {},
1316
+ }
1317
+ )
1318
+ except Exception as e:
1319
+ self.log.error(f"[FEATURE FLAGS] Cache provider store error: {e}")
1320
+ # Flags are already in memory, so continue normally
1199
1321
 
1200
1322
  except APIError as e:
1201
1323
  if e.status == 401:
@@ -1295,7 +1417,8 @@ class Client(object):
1295
1417
  flag_filters = feature_flag.get("filters") or {}
1296
1418
  aggregation_group_type_index = flag_filters.get("aggregation_group_type_index")
1297
1419
  if aggregation_group_type_index is not None:
1298
- group_name = self.group_type_mapping.get(str(aggregation_group_type_index))
1420
+ group_type_mapping = self.group_type_mapping or {}
1421
+ group_name = group_type_mapping.get(str(aggregation_group_type_index))
1299
1422
 
1300
1423
  if not group_name:
1301
1424
  self.log.warning(
@@ -1387,6 +1510,19 @@ class Client(object):
1387
1510
  return None
1388
1511
  return bool(response)
1389
1512
 
1513
+ def _get_stale_flag_fallback(
1514
+ self, distinct_id: ID_TYPES, key: str
1515
+ ) -> Optional[FeatureFlagResult]:
1516
+ """Returns a stale cached flag value if available, otherwise None."""
1517
+ if self.flag_cache:
1518
+ stale_result = self.flag_cache.get_stale_cached_flag(distinct_id, key)
1519
+ if stale_result:
1520
+ self.log.info(
1521
+ f"[FEATURE FLAGS] Using stale cached value for flag {key}"
1522
+ )
1523
+ return stale_result
1524
+ return None
1525
+
1390
1526
  def _get_feature_flag_result(
1391
1527
  self,
1392
1528
  key: str,
@@ -1419,6 +1555,8 @@ class Client(object):
1419
1555
  flag_result = None
1420
1556
  flag_details = None
1421
1557
  request_id = None
1558
+ evaluated_at = None
1559
+ feature_flag_error: Optional[str] = None
1422
1560
 
1423
1561
  flag_value = self._locally_evaluate_flag(
1424
1562
  key, distinct_id, groups, person_properties, group_properties
@@ -1443,14 +1581,24 @@ class Client(object):
1443
1581
  )
1444
1582
  elif not only_evaluate_locally:
1445
1583
  try:
1446
- flag_details, request_id = self._get_feature_flag_details_from_server(
1447
- key,
1448
- distinct_id,
1449
- groups,
1450
- person_properties,
1451
- group_properties,
1452
- disable_geoip,
1584
+ flag_details, request_id, evaluated_at, errors_while_computing = (
1585
+ self._get_feature_flag_details_from_server(
1586
+ key,
1587
+ distinct_id,
1588
+ groups,
1589
+ person_properties,
1590
+ group_properties,
1591
+ disable_geoip,
1592
+ )
1453
1593
  )
1594
+ errors = []
1595
+ if errors_while_computing:
1596
+ errors.append(FeatureFlagError.ERRORS_WHILE_COMPUTING)
1597
+ if flag_details is None:
1598
+ errors.append(FeatureFlagError.FLAG_MISSING)
1599
+ if errors:
1600
+ feature_flag_error = ",".join(errors)
1601
+
1454
1602
  flag_result = FeatureFlagResult.from_flag_details(
1455
1603
  flag_details, override_match_value
1456
1604
  )
@@ -1464,19 +1612,26 @@ class Client(object):
1464
1612
  self.log.debug(
1465
1613
  f"Successfully computed flag remotely: #{key} -> #{flag_result}"
1466
1614
  )
1615
+ except QuotaLimitError as e:
1616
+ self.log.warning(f"[FEATURE FLAGS] Quota limit exceeded: {e}")
1617
+ feature_flag_error = FeatureFlagError.QUOTA_LIMITED
1618
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1619
+ except RequestsTimeout as e:
1620
+ self.log.warning(f"[FEATURE FLAGS] Request timed out: {e}")
1621
+ feature_flag_error = FeatureFlagError.TIMEOUT
1622
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1623
+ except RequestsConnectionError as e:
1624
+ self.log.warning(f"[FEATURE FLAGS] Connection error: {e}")
1625
+ feature_flag_error = FeatureFlagError.CONNECTION_ERROR
1626
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1627
+ except APIError as e:
1628
+ self.log.warning(f"[FEATURE FLAGS] API error: {e}")
1629
+ feature_flag_error = FeatureFlagError.api_error(e.status)
1630
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1467
1631
  except Exception as e:
1468
1632
  self.log.exception(f"[FEATURE FLAGS] Unable to get flag remotely: {e}")
1469
-
1470
- # Fallback to cached value if remote evaluation fails
1471
- if self.flag_cache:
1472
- stale_result = self.flag_cache.get_stale_cached_flag(
1473
- distinct_id, key
1474
- )
1475
- if stale_result:
1476
- self.log.info(
1477
- f"[FEATURE FLAGS] Using stale cached value for flag {key}"
1478
- )
1479
- flag_result = stale_result
1633
+ feature_flag_error = FeatureFlagError.UNKNOWN_ERROR
1634
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1480
1635
 
1481
1636
  if send_feature_flag_events:
1482
1637
  self._capture_feature_flag_called(
@@ -1488,7 +1643,9 @@ class Client(object):
1488
1643
  groups,
1489
1644
  disable_geoip,
1490
1645
  request_id,
1646
+ evaluated_at,
1491
1647
  flag_details,
1648
+ feature_flag_error,
1492
1649
  )
1493
1650
 
1494
1651
  return flag_result
@@ -1691,9 +1848,10 @@ class Client(object):
1691
1848
  person_properties: dict[str, str],
1692
1849
  group_properties: dict[str, str],
1693
1850
  disable_geoip: Optional[bool],
1694
- ) -> tuple[Optional[FeatureFlag], Optional[str]]:
1851
+ ) -> tuple[Optional[FeatureFlag], Optional[str], Optional[int], bool]:
1695
1852
  """
1696
- Calls /flags and returns the flag details and request id
1853
+ Calls /flags and returns the flag details, request id, evaluated at timestamp,
1854
+ and whether there were errors while computing flags.
1697
1855
  """
1698
1856
  resp_data = self.get_flags_decision(
1699
1857
  distinct_id,
@@ -1704,9 +1862,11 @@ class Client(object):
1704
1862
  flag_keys_to_evaluate=[key],
1705
1863
  )
1706
1864
  request_id = resp_data.get("requestId")
1865
+ evaluated_at = resp_data.get("evaluatedAt")
1866
+ errors_while_computing = resp_data.get("errorsWhileComputingFlags", False)
1707
1867
  flags = resp_data.get("flags")
1708
1868
  flag_details = flags.get(key) if flags else None
1709
- return flag_details, request_id
1869
+ return flag_details, request_id, evaluated_at, errors_while_computing
1710
1870
 
1711
1871
  def _capture_feature_flag_called(
1712
1872
  self,
@@ -1718,7 +1878,9 @@ class Client(object):
1718
1878
  groups: Dict[str, str],
1719
1879
  disable_geoip: Optional[bool],
1720
1880
  request_id: Optional[str],
1881
+ evaluated_at: Optional[int],
1721
1882
  flag_details: Optional[FeatureFlag],
1883
+ feature_flag_error: Optional[str] = None,
1722
1884
  ):
1723
1885
  feature_flag_reported_key = (
1724
1886
  f"{key}_{'::null::' if response is None else str(response)}"
@@ -1741,6 +1903,8 @@ class Client(object):
1741
1903
 
1742
1904
  if request_id:
1743
1905
  properties["$feature_flag_request_id"] = request_id
1906
+ if evaluated_at:
1907
+ properties["$feature_flag_evaluated_at"] = evaluated_at
1744
1908
  if isinstance(flag_details, FeatureFlag):
1745
1909
  if flag_details.reason and flag_details.reason.description:
1746
1910
  properties["$feature_flag_reason"] = flag_details.reason.description
@@ -1751,6 +1915,8 @@ class Client(object):
1751
1915
  )
1752
1916
  if flag_details.metadata.id:
1753
1917
  properties["$feature_flag_id"] = flag_details.metadata.id
1918
+ if feature_flag_error:
1919
+ properties["$feature_flag_error"] = feature_flag_error
1754
1920
 
1755
1921
  self.capture(
1756
1922
  "$feature_flag_called",
@@ -54,6 +54,10 @@ DEFAULT_CODE_VARIABLES_MASK_PATTERNS = [
54
54
  r"(?i).*privatekey.*",
55
55
  r"(?i).*private_key.*",
56
56
  r"(?i).*token.*",
57
+ r"(?i).*aws_access_key_id.*",
58
+ r"(?i).*_pass",
59
+ r"(?i)sk_.*",
60
+ r"(?i).*jwt.*",
57
61
  ]
58
62
 
59
63
  DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS = [r"^__.*"]
@@ -941,7 +945,31 @@ def _pattern_matches(name, patterns):
941
945
  return False
942
946
 
943
947
 
944
- def _serialize_variable_value(value, limiter, max_length=1024):
948
+ def _mask_sensitive_data(value, compiled_mask):
949
+ if not compiled_mask:
950
+ return value
951
+
952
+ if isinstance(value, dict):
953
+ result = {}
954
+ for k, v in value.items():
955
+ key_str = str(k) if not isinstance(k, str) else k
956
+ if _pattern_matches(key_str, compiled_mask):
957
+ result[k] = CODE_VARIABLES_REDACTED_VALUE
958
+ else:
959
+ result[k] = _mask_sensitive_data(v, compiled_mask)
960
+ return result
961
+ elif isinstance(value, (list, tuple)):
962
+ masked_items = [_mask_sensitive_data(item, compiled_mask) for item in value]
963
+ return type(value)(masked_items)
964
+ elif isinstance(value, str):
965
+ if _pattern_matches(value, compiled_mask):
966
+ return CODE_VARIABLES_REDACTED_VALUE
967
+ return value
968
+ else:
969
+ return value
970
+
971
+
972
+ def _serialize_variable_value(value, limiter, max_length=1024, compiled_mask=None):
945
973
  try:
946
974
  if value is None:
947
975
  result = "None"
@@ -954,9 +982,13 @@ def _serialize_variable_value(value, limiter, max_length=1024):
954
982
  limiter.add(result_size)
955
983
  return value
956
984
  elif isinstance(value, str):
957
- result = value
985
+ if compiled_mask and _pattern_matches(value, compiled_mask):
986
+ result = CODE_VARIABLES_REDACTED_VALUE
987
+ else:
988
+ result = value
958
989
  else:
959
- result = json.dumps(value)
990
+ masked_value = _mask_sensitive_data(value, compiled_mask)
991
+ result = json.dumps(masked_value)
960
992
 
961
993
  if len(result) > max_length:
962
994
  result = result[: max_length - 3] + "..."
@@ -1043,7 +1075,9 @@ def serialize_code_variables(
1043
1075
  limiter.add(redacted_size)
1044
1076
  result[name] = redacted_value
1045
1077
  else:
1046
- serialized = _serialize_variable_value(value, limiter, max_length)
1078
+ serialized = _serialize_variable_value(
1079
+ value, limiter, max_length, compiled_mask
1080
+ )
1047
1081
  if serialized is None:
1048
1082
  break
1049
1083
  result[name] = serialized
@@ -1053,6 +1087,17 @@ def serialize_code_variables(
1053
1087
 
1054
1088
  def try_attach_code_variables_to_frames(
1055
1089
  all_exceptions, exc_info, mask_patterns, ignore_patterns
1090
+ ):
1091
+ try:
1092
+ attach_code_variables_to_frames(
1093
+ all_exceptions, exc_info, mask_patterns, ignore_patterns
1094
+ )
1095
+ except Exception:
1096
+ pass
1097
+
1098
+
1099
+ def attach_code_variables_to_frames(
1100
+ all_exceptions, exc_info, mask_patterns, ignore_patterns
1056
1101
  ):
1057
1102
  exc_type, exc_value, traceback = exc_info
1058
1103