sentry-sdk 2.26.1__py2.py3-none-any.whl → 3.0.0a1__py2.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.

Potentially problematic release.


This version of sentry-sdk might be problematic. Click here for more details.

Files changed (114) hide show
  1. sentry_sdk/__init__.py +4 -8
  2. sentry_sdk/_compat.py +0 -1
  3. sentry_sdk/_init_implementation.py +6 -44
  4. sentry_sdk/_log_batcher.py +47 -28
  5. sentry_sdk/_types.py +8 -64
  6. sentry_sdk/ai/monitoring.py +14 -10
  7. sentry_sdk/ai/utils.py +1 -1
  8. sentry_sdk/api.py +69 -163
  9. sentry_sdk/client.py +25 -72
  10. sentry_sdk/consts.py +42 -23
  11. sentry_sdk/debug.py +0 -10
  12. sentry_sdk/envelope.py +2 -10
  13. sentry_sdk/feature_flags.py +5 -1
  14. sentry_sdk/integrations/__init__.py +5 -2
  15. sentry_sdk/integrations/_asgi_common.py +3 -3
  16. sentry_sdk/integrations/_wsgi_common.py +11 -40
  17. sentry_sdk/integrations/aiohttp.py +104 -57
  18. sentry_sdk/integrations/anthropic.py +10 -7
  19. sentry_sdk/integrations/arq.py +24 -13
  20. sentry_sdk/integrations/asgi.py +103 -83
  21. sentry_sdk/integrations/asyncio.py +1 -0
  22. sentry_sdk/integrations/asyncpg.py +45 -30
  23. sentry_sdk/integrations/aws_lambda.py +109 -92
  24. sentry_sdk/integrations/boto3.py +38 -9
  25. sentry_sdk/integrations/bottle.py +1 -1
  26. sentry_sdk/integrations/celery/__init__.py +48 -38
  27. sentry_sdk/integrations/clickhouse_driver.py +59 -28
  28. sentry_sdk/integrations/cohere.py +2 -0
  29. sentry_sdk/integrations/django/__init__.py +25 -46
  30. sentry_sdk/integrations/django/asgi.py +6 -2
  31. sentry_sdk/integrations/django/caching.py +13 -22
  32. sentry_sdk/integrations/django/middleware.py +1 -0
  33. sentry_sdk/integrations/django/signals_handlers.py +3 -1
  34. sentry_sdk/integrations/django/templates.py +8 -12
  35. sentry_sdk/integrations/django/transactions.py +1 -6
  36. sentry_sdk/integrations/django/views.py +5 -2
  37. sentry_sdk/integrations/falcon.py +7 -25
  38. sentry_sdk/integrations/fastapi.py +3 -3
  39. sentry_sdk/integrations/flask.py +1 -1
  40. sentry_sdk/integrations/gcp.py +63 -38
  41. sentry_sdk/integrations/graphene.py +6 -13
  42. sentry_sdk/integrations/grpc/aio/client.py +14 -8
  43. sentry_sdk/integrations/grpc/aio/server.py +19 -21
  44. sentry_sdk/integrations/grpc/client.py +8 -6
  45. sentry_sdk/integrations/grpc/server.py +12 -14
  46. sentry_sdk/integrations/httpx.py +47 -12
  47. sentry_sdk/integrations/huey.py +26 -22
  48. sentry_sdk/integrations/huggingface_hub.py +1 -0
  49. sentry_sdk/integrations/langchain.py +22 -15
  50. sentry_sdk/integrations/launchdarkly.py +3 -3
  51. sentry_sdk/integrations/litestar.py +4 -2
  52. sentry_sdk/integrations/logging.py +12 -3
  53. sentry_sdk/integrations/openai.py +2 -0
  54. sentry_sdk/integrations/openfeature.py +3 -5
  55. sentry_sdk/integrations/pymongo.py +18 -25
  56. sentry_sdk/integrations/pyramid.py +1 -1
  57. sentry_sdk/integrations/quart.py +3 -3
  58. sentry_sdk/integrations/ray.py +23 -17
  59. sentry_sdk/integrations/redis/_async_common.py +30 -18
  60. sentry_sdk/integrations/redis/_sync_common.py +28 -18
  61. sentry_sdk/integrations/redis/modules/caches.py +13 -10
  62. sentry_sdk/integrations/redis/modules/queries.py +14 -11
  63. sentry_sdk/integrations/redis/rb.py +4 -4
  64. sentry_sdk/integrations/redis/redis.py +6 -6
  65. sentry_sdk/integrations/redis/redis_cluster.py +18 -16
  66. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +4 -4
  67. sentry_sdk/integrations/redis/utils.py +63 -19
  68. sentry_sdk/integrations/rq.py +68 -23
  69. sentry_sdk/integrations/rust_tracing.py +28 -43
  70. sentry_sdk/integrations/sanic.py +23 -13
  71. sentry_sdk/integrations/socket.py +9 -5
  72. sentry_sdk/integrations/sqlalchemy.py +8 -8
  73. sentry_sdk/integrations/starlette.py +11 -31
  74. sentry_sdk/integrations/starlite.py +4 -2
  75. sentry_sdk/integrations/stdlib.py +56 -9
  76. sentry_sdk/integrations/strawberry.py +40 -59
  77. sentry_sdk/integrations/threading.py +10 -26
  78. sentry_sdk/integrations/tornado.py +57 -18
  79. sentry_sdk/integrations/trytond.py +4 -1
  80. sentry_sdk/integrations/unleash.py +2 -3
  81. sentry_sdk/integrations/wsgi.py +84 -38
  82. sentry_sdk/opentelemetry/__init__.py +9 -0
  83. sentry_sdk/opentelemetry/consts.py +33 -0
  84. sentry_sdk/opentelemetry/contextvars_context.py +73 -0
  85. sentry_sdk/{integrations/opentelemetry → opentelemetry}/propagator.py +19 -28
  86. sentry_sdk/opentelemetry/sampler.py +326 -0
  87. sentry_sdk/opentelemetry/scope.py +218 -0
  88. sentry_sdk/opentelemetry/span_processor.py +329 -0
  89. sentry_sdk/opentelemetry/tracing.py +35 -0
  90. sentry_sdk/opentelemetry/utils.py +476 -0
  91. sentry_sdk/profiler/__init__.py +0 -40
  92. sentry_sdk/profiler/continuous_profiler.py +1 -30
  93. sentry_sdk/profiler/transaction_profiler.py +5 -56
  94. sentry_sdk/scope.py +107 -351
  95. sentry_sdk/sessions.py +0 -87
  96. sentry_sdk/tracing.py +418 -1134
  97. sentry_sdk/tracing_utils.py +134 -169
  98. sentry_sdk/transport.py +4 -104
  99. sentry_sdk/types.py +26 -2
  100. sentry_sdk/utils.py +169 -152
  101. {sentry_sdk-2.26.1.dist-info → sentry_sdk-3.0.0a1.dist-info}/METADATA +3 -5
  102. sentry_sdk-3.0.0a1.dist-info/RECORD +154 -0
  103. {sentry_sdk-2.26.1.dist-info → sentry_sdk-3.0.0a1.dist-info}/WHEEL +1 -1
  104. sentry_sdk-3.0.0a1.dist-info/entry_points.txt +2 -0
  105. sentry_sdk/hub.py +0 -739
  106. sentry_sdk/integrations/opentelemetry/__init__.py +0 -7
  107. sentry_sdk/integrations/opentelemetry/consts.py +0 -5
  108. sentry_sdk/integrations/opentelemetry/integration.py +0 -58
  109. sentry_sdk/integrations/opentelemetry/span_processor.py +0 -391
  110. sentry_sdk/metrics.py +0 -965
  111. sentry_sdk-2.26.1.dist-info/RECORD +0 -152
  112. sentry_sdk-2.26.1.dist-info/entry_points.txt +0 -2
  113. {sentry_sdk-2.26.1.dist-info → sentry_sdk-3.0.0a1.dist-info}/licenses/LICENSE +0 -0
  114. {sentry_sdk-2.26.1.dist-info → sentry_sdk-3.0.0a1.dist-info}/top_level.txt +0 -0
sentry_sdk/transport.py CHANGED
@@ -5,7 +5,6 @@ import gzip
5
5
  import socket
6
6
  import ssl
7
7
  import time
8
- import warnings
9
8
  from datetime import datetime, timedelta, timezone
10
9
  from collections import defaultdict
11
10
  from urllib.request import getproxies
@@ -18,7 +17,6 @@ except ImportError:
18
17
  import urllib3
19
18
  import certifi
20
19
 
21
- import sentry_sdk
22
20
  from sentry_sdk.consts import EndpointType
23
21
  from sentry_sdk.utils import Dsn, logger, capture_internal_exceptions
24
22
  from sentry_sdk.worker import BackgroundWorker
@@ -41,7 +39,7 @@ if TYPE_CHECKING:
41
39
  from urllib3.poolmanager import PoolManager
42
40
  from urllib3.poolmanager import ProxyManager
43
41
 
44
- from sentry_sdk._types import Event, EventDataCategory
42
+ from sentry_sdk._types import EventDataCategory
45
43
 
46
44
  KEEP_ALIVE_SOCKET_OPTIONS = []
47
45
  for option in [
@@ -74,25 +72,6 @@ class Transport(ABC):
74
72
  else:
75
73
  self.parsed_dsn = None
76
74
 
77
- def capture_event(self, event):
78
- # type: (Self, Event) -> None
79
- """
80
- DEPRECATED: Please use capture_envelope instead.
81
-
82
- This gets invoked with the event dictionary when an event should
83
- be sent to sentry.
84
- """
85
-
86
- warnings.warn(
87
- "capture_event is deprecated, please use capture_envelope instead!",
88
- DeprecationWarning,
89
- stacklevel=2,
90
- )
91
-
92
- envelope = Envelope()
93
- envelope.add_event(event)
94
- self.capture_envelope(envelope)
95
-
96
75
  @abstractmethod
97
76
  def capture_envelope(self, envelope):
98
77
  # type: (Self, Envelope) -> None
@@ -178,17 +157,8 @@ def _parse_rate_limits(header, now=None):
178
157
 
179
158
  retry_after = now + timedelta(seconds=int(retry_after_val))
180
159
  for category in categories and categories.split(";") or (None,):
181
- if category == "metric_bucket":
182
- try:
183
- namespaces = parameters[4].split(";")
184
- except IndexError:
185
- namespaces = []
186
-
187
- if not namespaces or "custom" in namespaces:
188
- yield category, retry_after # type: ignore
189
-
190
- else:
191
- yield category, retry_after # type: ignore
160
+ category = cast("Optional[EventDataCategory]", category)
161
+ yield category, retry_after
192
162
  except (LookupError, ValueError):
193
163
  continue
194
164
 
@@ -217,9 +187,6 @@ class BaseHttpTransport(Transport):
217
187
 
218
188
  self._pool = self._make_pool()
219
189
 
220
- # Backwards compatibility for deprecated `self.hub_class` attribute
221
- self._hub_cls = sentry_sdk.Hub
222
-
223
190
  experiments = options.get("_experiments", {})
224
191
  compression_level = experiments.get(
225
192
  "transport_compression_level",
@@ -426,12 +393,6 @@ class BaseHttpTransport(Transport):
426
393
  # type: (str) -> bool
427
394
  def _disabled(bucket):
428
395
  # type: (Any) -> bool
429
-
430
- # The envelope item type used for metrics is statsd
431
- # whereas the rate limit category is metric_bucket
432
- if bucket == "statsd":
433
- bucket = "metric_bucket"
434
-
435
396
  ts = self._disabled_until.get(bucket)
436
397
  return ts is not None and ts > datetime.now(timezone.utc)
437
398
 
@@ -458,7 +419,7 @@ class BaseHttpTransport(Transport):
458
419
  new_items = []
459
420
  for item in envelope.items:
460
421
  if self._check_disabled(item.data_category):
461
- if item.data_category in ("transaction", "error", "default", "statsd"):
422
+ if item.data_category in ("transaction", "error", "default"):
462
423
  self.on_dropped_event("self_rate_limits")
463
424
  self.record_lost_event("ratelimit_backoff", item=item)
464
425
  else:
@@ -587,30 +548,6 @@ class BaseHttpTransport(Transport):
587
548
  logger.debug("Killing HTTP transport")
588
549
  self._worker.kill()
589
550
 
590
- @staticmethod
591
- def _warn_hub_cls():
592
- # type: () -> None
593
- """Convenience method to warn users about the deprecation of the `hub_cls` attribute."""
594
- warnings.warn(
595
- "The `hub_cls` attribute is deprecated and will be removed in a future release.",
596
- DeprecationWarning,
597
- stacklevel=3,
598
- )
599
-
600
- @property
601
- def hub_cls(self):
602
- # type: (Self) -> type[sentry_sdk.Hub]
603
- """DEPRECATED: This attribute is deprecated and will be removed in a future release."""
604
- HttpTransport._warn_hub_cls()
605
- return self._hub_cls
606
-
607
- @hub_cls.setter
608
- def hub_cls(self, value):
609
- # type: (Self, type[sentry_sdk.Hub]) -> None
610
- """DEPRECATED: This attribute is deprecated and will be removed in a future release."""
611
- HttpTransport._warn_hub_cls()
612
- self._hub_cls = value
613
-
614
551
 
615
552
  class HttpTransport(BaseHttpTransport):
616
553
  if TYPE_CHECKING:
@@ -862,35 +799,6 @@ else:
862
799
  return httpcore.ConnectionPool(**opts)
863
800
 
864
801
 
865
- class _FunctionTransport(Transport):
866
- """
867
- DEPRECATED: Users wishing to provide a custom transport should subclass
868
- the Transport class, rather than providing a function.
869
- """
870
-
871
- def __init__(
872
- self, func # type: Callable[[Event], None]
873
- ):
874
- # type: (...) -> None
875
- Transport.__init__(self)
876
- self._func = func
877
-
878
- def capture_event(
879
- self, event # type: Event
880
- ):
881
- # type: (...) -> None
882
- self._func(event)
883
- return None
884
-
885
- def capture_envelope(self, envelope: Envelope) -> None:
886
- # Since function transports expect to be called with an event, we need
887
- # to iterate over the envelope and call the function for each event, via
888
- # the deprecated capture_event method.
889
- event = envelope.get_event()
890
- if event is not None:
891
- self.capture_event(event)
892
-
893
-
894
802
  def make_transport(options):
895
803
  # type: (Dict[str, Any]) -> Optional[Transport]
896
804
  ref_transport = options["transport"]
@@ -906,14 +814,6 @@ def make_transport(options):
906
814
  return ref_transport
907
815
  elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport):
908
816
  transport_cls = ref_transport
909
- elif callable(ref_transport):
910
- warnings.warn(
911
- "Function transports are deprecated and will be removed in a future release."
912
- "Please provide a Transport instance or subclass, instead.",
913
- DeprecationWarning,
914
- stacklevel=2,
915
- )
916
- return _FunctionTransport(ref_transport)
917
817
 
918
818
  # if a transport class is given only instantiate it if the dsn is not
919
819
  # empty or None
sentry_sdk/types.py CHANGED
@@ -11,15 +11,39 @@ releases.
11
11
  from typing import TYPE_CHECKING
12
12
 
13
13
  if TYPE_CHECKING:
14
- from sentry_sdk._types import Event, EventDataCategory, Hint, Log
14
+ # Re-export types to make them available in the public API
15
+ from sentry_sdk._types import (
16
+ Breadcrumb,
17
+ BreadcrumbHint,
18
+ Event,
19
+ EventDataCategory,
20
+ Hint,
21
+ Log,
22
+ MonitorConfig,
23
+ SamplingContext,
24
+ )
15
25
  else:
16
26
  from typing import Any
17
27
 
18
28
  # The lines below allow the types to be imported from outside `if TYPE_CHECKING`
19
29
  # guards. The types in this module are only intended to be used for type hints.
30
+ Breadcrumb = Any
31
+ BreadcrumbHint = Any
20
32
  Event = Any
21
33
  EventDataCategory = Any
22
34
  Hint = Any
23
35
  Log = Any
36
+ MonitorConfig = Any
37
+ SamplingContext = Any
24
38
 
25
- __all__ = ("Event", "EventDataCategory", "Hint", "Log")
39
+
40
+ __all__ = (
41
+ "Breadcrumb",
42
+ "BreadcrumbHint",
43
+ "Event",
44
+ "EventDataCategory",
45
+ "Hint",
46
+ "Log",
47
+ "MonitorConfig",
48
+ "SamplingContext",
49
+ )
sentry_sdk/utils.py CHANGED
@@ -25,11 +25,11 @@ except ImportError:
25
25
  BaseExceptionGroup = None # type: ignore
26
26
 
27
27
  import sentry_sdk
28
- from sentry_sdk._compat import PY37
29
28
  from sentry_sdk.consts import (
30
29
  DEFAULT_ADD_FULL_STACK,
31
30
  DEFAULT_MAX_STACK_FRAMES,
32
31
  DEFAULT_MAX_VALUE_LENGTH,
32
+ SPANDATA,
33
33
  EndpointType,
34
34
  )
35
35
  from sentry_sdk._types import Annotated, AnnotatedValue, SENSITIVE_DATA_SUBSTITUTE
@@ -57,7 +57,8 @@ if TYPE_CHECKING:
57
57
  Union,
58
58
  )
59
59
 
60
- from gevent.hub import Hub
60
+ from gevent.hub import Hub as GeventHub
61
+ from opentelemetry.util.types import AttributeValue
61
62
 
62
63
  from sentry_sdk._types import Event, ExcInfo
63
64
 
@@ -86,6 +87,13 @@ exceeds the default sys.getrecursionlimit() of 1000, so users will only
86
87
  be affected by this limit if they have a custom recursion limit.
87
88
  """
88
89
 
90
+ MAX_EXCEPTIONS = 25
91
+ """Maximum number of exceptions in a chain or group to send to Sentry.
92
+
93
+ This is a sanity limit to avoid ending in an infinite loop of exceptions when the same exception is in the root and a leave
94
+ of the exception tree.
95
+ """
96
+
89
97
 
90
98
  def env_to_bool(value, *, strict=False):
91
99
  # type: (Any, Optional[bool]) -> bool | None
@@ -250,31 +258,6 @@ def format_timestamp(value):
250
258
  return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
251
259
 
252
260
 
253
- ISO_TZ_SEPARATORS = frozenset(("+", "-"))
254
-
255
-
256
- def datetime_from_isoformat(value):
257
- # type: (str) -> datetime
258
- try:
259
- result = datetime.fromisoformat(value)
260
- except (AttributeError, ValueError):
261
- # py 3.6
262
- timestamp_format = (
263
- "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S"
264
- )
265
- if value.endswith("Z"):
266
- value = value[:-1] + "+0000"
267
-
268
- if value[-6] in ISO_TZ_SEPARATORS:
269
- timestamp_format += "%z"
270
- value = value[:-3] + value[-2:]
271
- elif value[-5] in ISO_TZ_SEPARATORS:
272
- timestamp_format += "%z"
273
-
274
- result = datetime.strptime(value, timestamp_format)
275
- return result.astimezone(timezone.utc)
276
-
277
-
278
261
  def event_hint_with_exc_info(exc_info=None):
279
262
  # type: (Optional[ExcInfo]) -> Dict[str, Optional[ExcInfo]]
280
263
  """Creates a hint with the exc info filled in."""
@@ -822,14 +805,17 @@ def exceptions_from_error(
822
805
  ):
823
806
  # type: (...) -> Tuple[int, List[Dict[str, Any]]]
824
807
  """
825
- Creates the list of exceptions.
826
- This can include chained exceptions and exceptions from an ExceptionGroup.
827
-
828
- See the Exception Interface documentation for more details:
829
- https://develop.sentry.dev/sdk/event-payloads/exception/
808
+ Converts the given exception information into the Sentry structured "exception" format.
809
+ This will return a list of exceptions (a flattened tree of exceptions) in the
810
+ format of the Exception Interface documentation:
811
+ https://develop.sentry.dev/sdk/data-model/event-payloads/exception/
812
+
813
+ This function can handle:
814
+ - simple exceptions
815
+ - chained exceptions (raise .. from ..)
816
+ - exception groups
830
817
  """
831
-
832
- parent = single_exception_from_error_tuple(
818
+ base_exception = single_exception_from_error_tuple(
833
819
  exc_type=exc_type,
834
820
  exc_value=exc_value,
835
821
  tb=tb,
@@ -840,64 +826,79 @@ def exceptions_from_error(
840
826
  source=source,
841
827
  full_stack=full_stack,
842
828
  )
843
- exceptions = [parent]
829
+ exceptions = [base_exception]
844
830
 
845
831
  parent_id = exception_id
846
832
  exception_id += 1
847
833
 
848
- should_supress_context = hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore
849
- if should_supress_context:
850
- # Add direct cause.
851
- # The field `__cause__` is set when raised with the exception (using the `from` keyword).
852
- exception_has_cause = (
834
+ if exception_id > MAX_EXCEPTIONS - 1:
835
+ return (exception_id, exceptions)
836
+
837
+ causing_exception = None
838
+ exception_source = None
839
+
840
+ # Add any causing exceptions, if present.
841
+ should_suppress_context = hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore
842
+ # Note: __suppress_context__ is True if the exception is raised with the `from` keyword.
843
+ if should_suppress_context:
844
+ # Explicitly chained exceptions (Like: raise NewException() from OriginalException())
845
+ # The field `__cause__` is set to OriginalException
846
+ has_explicit_causing_exception = (
853
847
  exc_value
854
848
  and hasattr(exc_value, "__cause__")
855
849
  and exc_value.__cause__ is not None
856
850
  )
857
- if exception_has_cause:
858
- cause = exc_value.__cause__ # type: ignore
859
- (exception_id, child_exceptions) = exceptions_from_error(
860
- exc_type=type(cause),
861
- exc_value=cause,
862
- tb=getattr(cause, "__traceback__", None),
863
- client_options=client_options,
864
- mechanism=mechanism,
865
- exception_id=exception_id,
866
- source="__cause__",
867
- full_stack=full_stack,
868
- )
869
- exceptions.extend(child_exceptions)
870
-
851
+ if has_explicit_causing_exception:
852
+ exception_source = "__cause__"
853
+ causing_exception = exc_value.__cause__ # type: ignore
871
854
  else:
872
- # Add indirect cause.
873
- # The field `__context__` is assigned if another exception occurs while handling the exception.
874
- exception_has_content = (
855
+ # Implicitly chained exceptions (when an exception occurs while handling another exception)
856
+ # The field `__context__` is set in the exception that occurs while handling another exception,
857
+ # to the other exception.
858
+ has_implicit_causing_exception = (
875
859
  exc_value
876
860
  and hasattr(exc_value, "__context__")
877
861
  and exc_value.__context__ is not None
878
862
  )
879
- if exception_has_content:
880
- context = exc_value.__context__ # type: ignore
881
- (exception_id, child_exceptions) = exceptions_from_error(
882
- exc_type=type(context),
883
- exc_value=context,
884
- tb=getattr(context, "__traceback__", None),
885
- client_options=client_options,
886
- mechanism=mechanism,
887
- exception_id=exception_id,
888
- source="__context__",
889
- full_stack=full_stack,
890
- )
891
- exceptions.extend(child_exceptions)
863
+ if has_implicit_causing_exception:
864
+ exception_source = "__context__"
865
+ causing_exception = exc_value.__context__ # type: ignore
866
+
867
+ if causing_exception:
868
+ # Some frameworks (e.g. FastAPI) wrap the causing exception in an
869
+ # ExceptionGroup that only contain one exception: the causing exception.
870
+ # This would lead to an infinite loop, so we skip the causing exception
871
+ # in this case. (because it is the same as the base_exception above)
872
+ if (
873
+ BaseExceptionGroup is not None
874
+ and isinstance(causing_exception, BaseExceptionGroup)
875
+ and len(causing_exception.exceptions) == 1
876
+ and causing_exception.exceptions[0] == exc_value
877
+ ):
878
+ causing_exception = None
879
+
880
+ if causing_exception:
881
+ (exception_id, child_exceptions) = exceptions_from_error(
882
+ exc_type=type(causing_exception),
883
+ exc_value=causing_exception,
884
+ tb=getattr(causing_exception, "__traceback__", None),
885
+ client_options=client_options,
886
+ mechanism=mechanism,
887
+ exception_id=exception_id,
888
+ parent_id=parent_id,
889
+ source=exception_source,
890
+ full_stack=full_stack,
891
+ )
892
+ exceptions.extend(child_exceptions)
892
893
 
893
- # Add exceptions from an ExceptionGroup.
894
+ # Add child exceptions from an ExceptionGroup.
894
895
  is_exception_group = exc_value and hasattr(exc_value, "exceptions")
895
896
  if is_exception_group:
896
- for idx, e in enumerate(exc_value.exceptions): # type: ignore
897
+ for idx, causing_exception in enumerate(exc_value.exceptions): # type: ignore
897
898
  (exception_id, child_exceptions) = exceptions_from_error(
898
- exc_type=type(e),
899
- exc_value=e,
900
- tb=getattr(e, "__traceback__", None),
899
+ exc_type=type(causing_exception),
900
+ exc_value=causing_exception,
901
+ tb=getattr(causing_exception, "__traceback__", None),
901
902
  client_options=client_options,
902
903
  mechanism=mechanism,
903
904
  exception_id=exception_id,
@@ -917,38 +918,29 @@ def exceptions_from_error_tuple(
917
918
  full_stack=None, # type: Optional[list[dict[str, Any]]]
918
919
  ):
919
920
  # type: (...) -> List[Dict[str, Any]]
921
+ """
922
+ Convert Python's exception information into Sentry's structured "exception" format in the event.
923
+ See https://develop.sentry.dev/sdk/data-model/event-payloads/exception/
924
+ This is the entry point for the exception handling.
925
+ """
926
+ # unpack the exception info tuple
920
927
  exc_type, exc_value, tb = exc_info
921
928
 
922
- is_exception_group = BaseExceptionGroup is not None and isinstance(
923
- exc_value, BaseExceptionGroup
929
+ # let exceptions_from_error do the actual work
930
+ _, exceptions = exceptions_from_error(
931
+ exc_type=exc_type,
932
+ exc_value=exc_value,
933
+ tb=tb,
934
+ client_options=client_options,
935
+ mechanism=mechanism,
936
+ exception_id=0,
937
+ parent_id=0,
938
+ full_stack=full_stack,
924
939
  )
925
940
 
926
- if is_exception_group:
927
- (_, exceptions) = exceptions_from_error(
928
- exc_type=exc_type,
929
- exc_value=exc_value,
930
- tb=tb,
931
- client_options=client_options,
932
- mechanism=mechanism,
933
- exception_id=0,
934
- parent_id=0,
935
- full_stack=full_stack,
936
- )
937
-
938
- else:
939
- exceptions = []
940
- for exc_type, exc_value, tb in walk_exception_chain(exc_info):
941
- exceptions.append(
942
- single_exception_from_error_tuple(
943
- exc_type=exc_type,
944
- exc_value=exc_value,
945
- tb=tb,
946
- client_options=client_options,
947
- mechanism=mechanism,
948
- full_stack=full_stack,
949
- )
950
- )
951
-
941
+ # make sure the exceptions are sorted
942
+ # from the innermost (oldest)
943
+ # to the outermost (newest) exception
952
944
  exceptions.reverse()
953
945
 
954
946
  return exceptions
@@ -1372,27 +1364,13 @@ def _get_contextvars():
1372
1364
  See https://docs.sentry.io/platforms/python/contextvars/ for more information.
1373
1365
  """
1374
1366
  if not _is_contextvars_broken():
1375
- # aiocontextvars is a PyPI package that ensures that the contextvars
1376
- # backport (also a PyPI package) works with asyncio under Python 3.6
1377
- #
1378
- # Import it if available.
1379
- if sys.version_info < (3, 7):
1380
- # `aiocontextvars` is absolutely required for functional
1381
- # contextvars on Python 3.6.
1382
- try:
1383
- from aiocontextvars import ContextVar
1384
-
1385
- return True, ContextVar
1386
- except ImportError:
1387
- pass
1388
- else:
1389
- # On Python 3.7 contextvars are functional.
1390
- try:
1391
- from contextvars import ContextVar
1367
+ # On Python 3.7+ contextvars are functional.
1368
+ try:
1369
+ from contextvars import ContextVar
1392
1370
 
1393
- return True, ContextVar
1394
- except ImportError:
1395
- pass
1371
+ return True, ContextVar
1372
+ except ImportError:
1373
+ pass
1396
1374
 
1397
1375
  # Fall back to basic thread-local usage.
1398
1376
 
@@ -1792,7 +1770,7 @@ def ensure_integration_enabled(
1792
1770
  ```python
1793
1771
  @ensure_integration_enabled(MyIntegration, my_function)
1794
1772
  def patch_my_function():
1795
- with sentry_sdk.start_transaction(...):
1773
+ with sentry_sdk.start_span(...):
1796
1774
  return my_function()
1797
1775
  ```
1798
1776
  """
@@ -1818,19 +1796,6 @@ def ensure_integration_enabled(
1818
1796
  return patcher
1819
1797
 
1820
1798
 
1821
- if PY37:
1822
-
1823
- def nanosecond_time():
1824
- # type: () -> int
1825
- return time.perf_counter_ns()
1826
-
1827
- else:
1828
-
1829
- def nanosecond_time():
1830
- # type: () -> int
1831
- return int(time.perf_counter() * 1e9)
1832
-
1833
-
1834
1799
  def now():
1835
1800
  # type: () -> float
1836
1801
  return time.perf_counter()
@@ -1842,9 +1807,9 @@ try:
1842
1807
  except ImportError:
1843
1808
 
1844
1809
  # it's not great that the signatures are different, get_hub can't return None
1845
- # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
1810
+ # consider adding an if TYPE_CHECKING to change the signature to Optional[GeventHub]
1846
1811
  def get_gevent_hub(): # type: ignore[misc]
1847
- # type: () -> Optional[Hub]
1812
+ # type: () -> Optional[GeventHub]
1848
1813
  return None
1849
1814
 
1850
1815
  def is_module_patched(mod_name):
@@ -1909,6 +1874,56 @@ def get_current_thread_meta(thread=None):
1909
1874
  return None, None
1910
1875
 
1911
1876
 
1877
+ def _serialize_span_attribute(value):
1878
+ # type: (Any) -> Optional[AttributeValue]
1879
+ """Serialize an object so that it's OTel-compatible and displays nicely in Sentry."""
1880
+ # check for allowed primitives
1881
+ if isinstance(value, (int, str, float, bool)):
1882
+ return value
1883
+
1884
+ # lists are allowed too, as long as they don't mix types
1885
+ if isinstance(value, (list, tuple)):
1886
+ for type_ in (int, str, float, bool):
1887
+ if all(isinstance(item, type_) for item in value):
1888
+ return list(value)
1889
+
1890
+ # if this is anything else, just try to coerce to string
1891
+ # we prefer json.dumps since this makes things like dictionaries display
1892
+ # nicely in the UI
1893
+ try:
1894
+ return json.dumps(value)
1895
+ except TypeError:
1896
+ try:
1897
+ return str(value)
1898
+ except Exception:
1899
+ return None
1900
+
1901
+
1902
+ ISO_TZ_SEPARATORS = frozenset(("+", "-"))
1903
+
1904
+
1905
+ def datetime_from_isoformat(value):
1906
+ # type: (str) -> datetime
1907
+ try:
1908
+ result = datetime.fromisoformat(value)
1909
+ except (AttributeError, ValueError):
1910
+ # py 3.6
1911
+ timestamp_format = (
1912
+ "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S"
1913
+ )
1914
+ if value.endswith("Z"):
1915
+ value = value[:-1] + "+0000"
1916
+
1917
+ if value[-6] in ISO_TZ_SEPARATORS:
1918
+ timestamp_format += "%z"
1919
+ value = value[:-3] + value[-2:]
1920
+ elif value[-5] in ISO_TZ_SEPARATORS:
1921
+ timestamp_format += "%z"
1922
+
1923
+ result = datetime.strptime(value, timestamp_format)
1924
+ return result.astimezone(timezone.utc)
1925
+
1926
+
1912
1927
  def should_be_treated_as_error(ty, value):
1913
1928
  # type: (Any, Any) -> bool
1914
1929
  if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
@@ -1918,18 +1933,20 @@ def should_be_treated_as_error(ty, value):
1918
1933
  return True
1919
1934
 
1920
1935
 
1921
- if TYPE_CHECKING:
1922
- T = TypeVar("T")
1936
+ def http_client_status_to_breadcrumb_level(status_code):
1937
+ # type: (Optional[int]) -> str
1938
+ if status_code is not None:
1939
+ if 500 <= status_code <= 599:
1940
+ return "error"
1941
+ elif 400 <= status_code <= 499:
1942
+ return "warning"
1923
1943
 
1944
+ return "info"
1924
1945
 
1925
- def try_convert(convert_func, value):
1926
- # type: (Callable[[Any], T], Any) -> Optional[T]
1927
- """
1928
- Attempt to convert from an unknown type to a specific type, using the
1929
- given function. Return None if the conversion fails, i.e. if the function
1930
- raises an exception.
1931
- """
1932
- try:
1933
- return convert_func(value)
1934
- except Exception:
1935
- return None
1946
+
1947
+ def set_thread_info_from_span(data, span):
1948
+ # type: (Dict[str, Any], sentry_sdk.tracing.Span) -> None
1949
+ if span.get_attribute(SPANDATA.THREAD_ID) is not None:
1950
+ data[SPANDATA.THREAD_ID] = span.get_attribute(SPANDATA.THREAD_ID)
1951
+ if span.get_attribute(SPANDATA.THREAD_NAME) is not None:
1952
+ data[SPANDATA.THREAD_NAME] = span.get_attribute(SPANDATA.THREAD_NAME)