posthoganalytics 7.0.1__py3-none-any.whl → 7.4.1__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.
Files changed (28) hide show
  1. posthoganalytics/__init__.py +10 -0
  2. posthoganalytics/ai/gemini/__init__.py +3 -0
  3. posthoganalytics/ai/gemini/gemini.py +1 -1
  4. posthoganalytics/ai/gemini/gemini_async.py +423 -0
  5. posthoganalytics/ai/gemini/gemini_converter.py +87 -21
  6. posthoganalytics/ai/openai/openai.py +27 -2
  7. posthoganalytics/ai/openai/openai_async.py +27 -2
  8. posthoganalytics/ai/openai/openai_converter.py +6 -0
  9. posthoganalytics/ai/sanitization.py +27 -5
  10. posthoganalytics/ai/utils.py +2 -2
  11. posthoganalytics/client.py +224 -58
  12. posthoganalytics/exception_utils.py +49 -4
  13. posthoganalytics/flag_definition_cache.py +127 -0
  14. posthoganalytics/request.py +203 -23
  15. posthoganalytics/test/test_client.py +207 -22
  16. posthoganalytics/test/test_exception_capture.py +45 -1
  17. posthoganalytics/test/test_feature_flag_result.py +441 -2
  18. posthoganalytics/test/test_feature_flags.py +166 -73
  19. posthoganalytics/test/test_flag_definition_cache.py +612 -0
  20. posthoganalytics/test/test_request.py +536 -0
  21. posthoganalytics/test/test_utils.py +4 -1
  22. posthoganalytics/types.py +40 -0
  23. posthoganalytics/version.py +1 -1
  24. {posthoganalytics-7.0.1.dist-info → posthoganalytics-7.4.1.dist-info}/METADATA +2 -1
  25. {posthoganalytics-7.0.1.dist-info → posthoganalytics-7.4.1.dist-info}/RECORD +28 -25
  26. {posthoganalytics-7.0.1.dist-info → posthoganalytics-7.4.1.dist-info}/WHEEL +0 -0
  27. {posthoganalytics-7.0.1.dist-info → posthoganalytics-7.4.1.dist-info}/licenses/LICENSE +0 -0
  28. {posthoganalytics-7.0.1.dist-info → posthoganalytics-7.4.1.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,7 @@ import atexit
2
2
  import logging
3
3
  import os
4
4
  import sys
5
+ import warnings
5
6
  from datetime import datetime, timedelta
6
7
  from typing import Any, Dict, Optional, Union
7
8
  from typing_extensions import Unpack
@@ -28,10 +29,17 @@ from posthoganalytics.feature_flags import (
28
29
  RequiresServerEvaluation,
29
30
  match_feature_flag_properties,
30
31
  )
32
+ from posthoganalytics.flag_definition_cache import (
33
+ FlagDefinitionCacheData,
34
+ FlagDefinitionCacheProvider,
35
+ )
31
36
  from posthoganalytics.poller import Poller
32
37
  from posthoganalytics.request import (
33
38
  DEFAULT_HOST,
34
39
  APIError,
40
+ QuotaLimitError,
41
+ RequestsConnectionError,
42
+ RequestsTimeout,
35
43
  batch_post,
36
44
  determine_server_host,
37
45
  flags,
@@ -49,6 +57,7 @@ from posthoganalytics.contexts import (
49
57
  )
50
58
  from posthoganalytics.types import (
51
59
  FeatureFlag,
60
+ FeatureFlagError,
52
61
  FeatureFlagResult,
53
62
  FlagMetadata,
54
63
  FlagsAndPayloads,
@@ -184,6 +193,7 @@ class Client(object):
184
193
  before_send=None,
185
194
  flag_fallback_cache_url=None,
186
195
  enable_local_evaluation=True,
196
+ flag_definition_cache_provider: Optional[FlagDefinitionCacheProvider] = None,
187
197
  capture_exception_code_variables=False,
188
198
  code_variables_mask_patterns=None,
189
199
  code_variables_ignore_patterns=None,
@@ -222,8 +232,8 @@ class Client(object):
222
232
  self.timeout = timeout
223
233
  self._feature_flags = None # private variable to store flags
224
234
  self.feature_flags_by_key = None
225
- self.group_type_mapping = None
226
- self.cohorts = None
235
+ self.group_type_mapping: Optional[dict[str, str]] = None
236
+ self.cohorts: Optional[dict[str, Any]] = None
227
237
  self.poll_interval = poll_interval
228
238
  self.feature_flags_request_timeout_seconds = (
229
239
  feature_flags_request_timeout_seconds
@@ -232,6 +242,8 @@ class Client(object):
232
242
  self.distinct_ids_feature_flags_reported = SizeLimitedDict(MAX_DICT_SIZE, set)
233
243
  self.flag_cache = self._initialize_flag_cache(flag_fallback_cache_url)
234
244
  self.flag_definition_version = 0
245
+ self._flags_etag: Optional[str] = None
246
+ self._flag_definition_cache_provider = flag_definition_cache_provider
235
247
  self.disabled = disabled
236
248
  self.disable_geoip = disable_geoip
237
249
  self.historical_migration = historical_migration
@@ -622,7 +634,28 @@ class Client(object):
622
634
  if flag_options["should_send"]:
623
635
  try:
624
636
  if flag_options["only_evaluate_locally"] is True:
625
- # Only use local evaluation
637
+ # Local evaluation explicitly requested
638
+ feature_variants = self.get_all_flags(
639
+ distinct_id,
640
+ groups=(groups or {}),
641
+ person_properties=flag_options["person_properties"],
642
+ group_properties=flag_options["group_properties"],
643
+ disable_geoip=disable_geoip,
644
+ only_evaluate_locally=True,
645
+ flag_keys_to_evaluate=flag_options["flag_keys_filter"],
646
+ )
647
+ elif flag_options["only_evaluate_locally"] is False:
648
+ # Remote evaluation explicitly requested
649
+ feature_variants = self.get_feature_variants(
650
+ distinct_id,
651
+ groups,
652
+ person_properties=flag_options["person_properties"],
653
+ group_properties=flag_options["group_properties"],
654
+ disable_geoip=disable_geoip,
655
+ flag_keys_to_evaluate=flag_options["flag_keys_filter"],
656
+ )
657
+ elif self.feature_flags:
658
+ # Local flags available, prefer local evaluation
626
659
  feature_variants = self.get_all_flags(
627
660
  distinct_id,
628
661
  groups=(groups or {}),
@@ -633,7 +666,7 @@ class Client(object):
633
666
  flag_keys_to_evaluate=flag_options["flag_keys_filter"],
634
667
  )
635
668
  else:
636
- # Default behavior - use remote evaluation
669
+ # Fall back to remote evaluation
637
670
  feature_variants = self.get_feature_variants(
638
671
  distinct_id,
639
672
  groups,
@@ -647,15 +680,6 @@ class Client(object):
647
680
  f"[FEATURE FLAGS] Unable to get feature variants: {e}"
648
681
  )
649
682
 
650
- elif self.feature_flags and event != "$feature_flag_called":
651
- # Local evaluation is enabled, flags are loaded, so try and get all flags we can without going to the server
652
- feature_variants = self.get_all_flags(
653
- distinct_id,
654
- groups=(groups or {}),
655
- disable_geoip=disable_geoip,
656
- only_evaluate_locally=True,
657
- )
658
-
659
683
  for feature, variant in (feature_variants or {}).items():
660
684
  extra_properties[f"$feature/{feature}"] = variant
661
685
 
@@ -979,10 +1003,6 @@ class Client(object):
979
1003
  all_exceptions_with_trace_and_in_app = event["exception"]["values"]
980
1004
 
981
1005
  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
1006
  "$exception_list": all_exceptions_with_trace_and_in_app,
987
1007
  **properties,
988
1008
  }
@@ -1147,17 +1167,25 @@ class Client(object):
1147
1167
  posthog.join()
1148
1168
  ```
1149
1169
  """
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
1170
+ if self.consumers:
1171
+ for consumer in self.consumers:
1172
+ consumer.pause()
1173
+ try:
1174
+ consumer.join()
1175
+ except RuntimeError:
1176
+ # consumer thread has not started
1177
+ pass
1157
1178
 
1158
1179
  if self.poller:
1159
1180
  self.poller.stop()
1160
1181
 
1182
+ # Shutdown the cache provider (release locks, cleanup)
1183
+ if self._flag_definition_cache_provider:
1184
+ try:
1185
+ self._flag_definition_cache_provider.shutdown()
1186
+ except Exception as e:
1187
+ self.log.error(f"[FEATURE FLAGS] Cache provider shutdown error: {e}")
1188
+
1161
1189
  def shutdown(self):
1162
1190
  """
1163
1191
  Flush all messages and cleanly shutdown the client. Call this before the process ends in serverless environments to avoid data loss.
@@ -1173,7 +1201,71 @@ class Client(object):
1173
1201
  if self.exception_capture:
1174
1202
  self.exception_capture.close()
1175
1203
 
1204
+ def _update_flag_state(
1205
+ self, data: FlagDefinitionCacheData, old_flags_by_key: Optional[dict] = None
1206
+ ) -> None:
1207
+ """Update internal flag state from cache data and invalidate evaluation cache if changed."""
1208
+ self.feature_flags = data["flags"]
1209
+ self.group_type_mapping = data["group_type_mapping"]
1210
+ self.cohorts = data["cohorts"]
1211
+
1212
+ # Invalidate evaluation cache if flag definitions changed
1213
+ if (
1214
+ self.flag_cache
1215
+ and old_flags_by_key is not None
1216
+ and old_flags_by_key != (self.feature_flags_by_key or {})
1217
+ ):
1218
+ old_version = self.flag_definition_version
1219
+ self.flag_definition_version += 1
1220
+ self.flag_cache.invalidate_version(old_version)
1221
+
1176
1222
  def _load_feature_flags(self):
1223
+ should_fetch = True
1224
+ if self._flag_definition_cache_provider:
1225
+ try:
1226
+ should_fetch = (
1227
+ self._flag_definition_cache_provider.should_fetch_flag_definitions()
1228
+ )
1229
+ except Exception as e:
1230
+ self.log.error(
1231
+ f"[FEATURE FLAGS] Cache provider should_fetch error: {e}"
1232
+ )
1233
+ # Fail-safe: fetch from API if cache provider errors
1234
+ should_fetch = True
1235
+
1236
+ # If not fetching, try to get from cache
1237
+ if not should_fetch and self._flag_definition_cache_provider:
1238
+ try:
1239
+ cached_data = (
1240
+ self._flag_definition_cache_provider.get_flag_definitions()
1241
+ )
1242
+ if cached_data:
1243
+ self.log.debug(
1244
+ "[FEATURE FLAGS] Using cached flag definitions from external cache"
1245
+ )
1246
+ self._update_flag_state(
1247
+ cached_data, old_flags_by_key=self.feature_flags_by_key or {}
1248
+ )
1249
+ self._last_feature_flag_poll = datetime.now(tz=tzutc())
1250
+ return
1251
+ else:
1252
+ # Emergency fallback: if cache is empty and we have no flags, fetch anyway.
1253
+ # There's really no other way of recovering in this case.
1254
+ if not self.feature_flags:
1255
+ self.log.debug(
1256
+ "[FEATURE FLAGS] Cache empty and no flags loaded, falling back to API fetch"
1257
+ )
1258
+ should_fetch = True
1259
+ except Exception as e:
1260
+ self.log.error(f"[FEATURE FLAGS] Cache provider get error: {e}")
1261
+ # Fail-safe: fetch from API if cache provider errors
1262
+ should_fetch = True
1263
+
1264
+ if should_fetch:
1265
+ self._fetch_feature_flags_from_api()
1266
+
1267
+ def _fetch_feature_flags_from_api(self):
1268
+ """Fetch feature flags from the PostHog API."""
1177
1269
  try:
1178
1270
  # Store old flags to detect changes
1179
1271
  old_flags_by_key: dict[str, dict] = self.feature_flags_by_key or {}
@@ -1183,19 +1275,41 @@ class Client(object):
1183
1275
  f"/api/feature_flag/local_evaluation/?token={self.api_key}&send_cohorts",
1184
1276
  self.host,
1185
1277
  timeout=10,
1278
+ etag=self._flags_etag,
1186
1279
  )
1187
1280
 
1188
- self.feature_flags = response["flags"] or []
1189
- self.group_type_mapping = response["group_type_mapping"] or {}
1190
- self.cohorts = response["cohorts"] or {}
1281
+ # Update stored ETag (clear if server stops sending one)
1282
+ self._flags_etag = response.etag
1191
1283
 
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)
1284
+ # If 304 Not Modified, flags haven't changed - skip processing
1285
+ if response.not_modified:
1286
+ self.log.debug(
1287
+ "[FEATURE FLAGS] Flags not modified (304), using cached data"
1288
+ )
1289
+ self._last_feature_flag_poll = datetime.now(tz=tzutc())
1290
+ return
1291
+
1292
+ if response.data is None:
1293
+ self.log.error(
1294
+ "[FEATURE FLAGS] Unexpected empty response data in non-304 response"
1295
+ )
1296
+ return
1297
+
1298
+ self._update_flag_state(response.data, old_flags_by_key=old_flags_by_key)
1299
+
1300
+ # Store in external cache if provider is configured
1301
+ if self._flag_definition_cache_provider:
1302
+ try:
1303
+ self._flag_definition_cache_provider.on_flag_definitions_received(
1304
+ {
1305
+ "flags": self.feature_flags or [],
1306
+ "group_type_mapping": self.group_type_mapping or {},
1307
+ "cohorts": self.cohorts or {},
1308
+ }
1309
+ )
1310
+ except Exception as e:
1311
+ self.log.error(f"[FEATURE FLAGS] Cache provider store error: {e}")
1312
+ # Flags are already in memory, so continue normally
1199
1313
 
1200
1314
  except APIError as e:
1201
1315
  if e.status == 401:
@@ -1295,7 +1409,8 @@ class Client(object):
1295
1409
  flag_filters = feature_flag.get("filters") or {}
1296
1410
  aggregation_group_type_index = flag_filters.get("aggregation_group_type_index")
1297
1411
  if aggregation_group_type_index is not None:
1298
- group_name = self.group_type_mapping.get(str(aggregation_group_type_index))
1412
+ group_type_mapping = self.group_type_mapping or {}
1413
+ group_name = group_type_mapping.get(str(aggregation_group_type_index))
1299
1414
 
1300
1415
  if not group_name:
1301
1416
  self.log.warning(
@@ -1387,6 +1502,19 @@ class Client(object):
1387
1502
  return None
1388
1503
  return bool(response)
1389
1504
 
1505
+ def _get_stale_flag_fallback(
1506
+ self, distinct_id: ID_TYPES, key: str
1507
+ ) -> Optional[FeatureFlagResult]:
1508
+ """Returns a stale cached flag value if available, otherwise None."""
1509
+ if self.flag_cache:
1510
+ stale_result = self.flag_cache.get_stale_cached_flag(distinct_id, key)
1511
+ if stale_result:
1512
+ self.log.info(
1513
+ f"[FEATURE FLAGS] Using stale cached value for flag {key}"
1514
+ )
1515
+ return stale_result
1516
+ return None
1517
+
1390
1518
  def _get_feature_flag_result(
1391
1519
  self,
1392
1520
  key: str,
@@ -1419,6 +1547,8 @@ class Client(object):
1419
1547
  flag_result = None
1420
1548
  flag_details = None
1421
1549
  request_id = None
1550
+ evaluated_at = None
1551
+ feature_flag_error: Optional[str] = None
1422
1552
 
1423
1553
  flag_value = self._locally_evaluate_flag(
1424
1554
  key, distinct_id, groups, person_properties, group_properties
@@ -1443,14 +1573,24 @@ class Client(object):
1443
1573
  )
1444
1574
  elif not only_evaluate_locally:
1445
1575
  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,
1576
+ flag_details, request_id, evaluated_at, errors_while_computing = (
1577
+ self._get_feature_flag_details_from_server(
1578
+ key,
1579
+ distinct_id,
1580
+ groups,
1581
+ person_properties,
1582
+ group_properties,
1583
+ disable_geoip,
1584
+ )
1453
1585
  )
1586
+ errors = []
1587
+ if errors_while_computing:
1588
+ errors.append(FeatureFlagError.ERRORS_WHILE_COMPUTING)
1589
+ if flag_details is None:
1590
+ errors.append(FeatureFlagError.FLAG_MISSING)
1591
+ if errors:
1592
+ feature_flag_error = ",".join(errors)
1593
+
1454
1594
  flag_result = FeatureFlagResult.from_flag_details(
1455
1595
  flag_details, override_match_value
1456
1596
  )
@@ -1464,19 +1604,26 @@ class Client(object):
1464
1604
  self.log.debug(
1465
1605
  f"Successfully computed flag remotely: #{key} -> #{flag_result}"
1466
1606
  )
1607
+ except QuotaLimitError as e:
1608
+ self.log.warning(f"[FEATURE FLAGS] Quota limit exceeded: {e}")
1609
+ feature_flag_error = FeatureFlagError.QUOTA_LIMITED
1610
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1611
+ except RequestsTimeout as e:
1612
+ self.log.warning(f"[FEATURE FLAGS] Request timed out: {e}")
1613
+ feature_flag_error = FeatureFlagError.TIMEOUT
1614
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1615
+ except RequestsConnectionError as e:
1616
+ self.log.warning(f"[FEATURE FLAGS] Connection error: {e}")
1617
+ feature_flag_error = FeatureFlagError.CONNECTION_ERROR
1618
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1619
+ except APIError as e:
1620
+ self.log.warning(f"[FEATURE FLAGS] API error: {e}")
1621
+ feature_flag_error = FeatureFlagError.api_error(e.status)
1622
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1467
1623
  except Exception as e:
1468
1624
  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
1625
+ feature_flag_error = FeatureFlagError.UNKNOWN_ERROR
1626
+ flag_result = self._get_stale_flag_fallback(distinct_id, key)
1480
1627
 
1481
1628
  if send_feature_flag_events:
1482
1629
  self._capture_feature_flag_called(
@@ -1488,7 +1635,9 @@ class Client(object):
1488
1635
  groups,
1489
1636
  disable_geoip,
1490
1637
  request_id,
1638
+ evaluated_at,
1491
1639
  flag_details,
1640
+ feature_flag_error,
1492
1641
  )
1493
1642
 
1494
1643
  return flag_result
@@ -1640,7 +1789,7 @@ class Client(object):
1640
1789
  person_properties=None,
1641
1790
  group_properties=None,
1642
1791
  only_evaluate_locally=False,
1643
- send_feature_flag_events=True,
1792
+ send_feature_flag_events=False,
1644
1793
  disable_geoip=None,
1645
1794
  ):
1646
1795
  """
@@ -1654,7 +1803,7 @@ class Client(object):
1654
1803
  person_properties: A dictionary of person properties.
1655
1804
  group_properties: A dictionary of group properties.
1656
1805
  only_evaluate_locally: Whether to only evaluate locally.
1657
- send_feature_flag_events: Whether to send feature flag events.
1806
+ send_feature_flag_events: Deprecated. Use get_feature_flag() instead if you need events.
1658
1807
  disable_geoip: Whether to disable GeoIP for this request.
1659
1808
 
1660
1809
  Examples:
@@ -1670,6 +1819,14 @@ class Client(object):
1670
1819
  Category:
1671
1820
  Feature flags
1672
1821
  """
1822
+ if send_feature_flag_events:
1823
+ warnings.warn(
1824
+ "send_feature_flag_events is deprecated in get_feature_flag_payload() and will be removed "
1825
+ "in a future version. Use get_feature_flag() if you want to send $feature_flag_called events.",
1826
+ DeprecationWarning,
1827
+ stacklevel=2,
1828
+ )
1829
+
1673
1830
  feature_flag_result = self._get_feature_flag_result(
1674
1831
  key,
1675
1832
  distinct_id,
@@ -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