sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.0__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.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -6
  2. sentry_sdk/_compat.py +64 -56
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +81 -19
  8. sentry_sdk/_types.py +311 -11
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +409 -67
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +849 -103
  16. sentry_sdk/consts.py +1389 -34
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +12 -15
  22. sentry_sdk/envelope.py +112 -61
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +442 -386
  25. sentry_sdk/integrations/__init__.py +228 -58
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +131 -40
  28. sentry_sdk/integrations/aiohttp.py +221 -72
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +4 -6
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +237 -135
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +13 -18
  37. sentry_sdk/integrations/aws_lambda.py +233 -80
  38. sentry_sdk/integrations/beam.py +27 -35
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +91 -69
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +35 -28
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +32 -8
  49. sentry_sdk/integrations/django/__init__.py +343 -89
  50. sentry_sdk/integrations/django/asgi.py +201 -22
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +80 -32
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +69 -2
  55. sentry_sdk/integrations/django/transactions.py +39 -14
  56. sentry_sdk/integrations/django/views.py +69 -16
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +19 -13
  59. sentry_sdk/integrations/executing.py +5 -6
  60. sentry_sdk/integrations/falcon.py +128 -65
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +114 -75
  63. sentry_sdk/integrations/gcp.py +67 -36
  64. sentry_sdk/integrations/gnu_backtrace.py +14 -22
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +261 -85
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +6 -33
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +20 -11
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +71 -60
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +62 -52
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +248 -114
  145. sentry_sdk/integrations/serverless.py +13 -22
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/spark_driver.py +115 -62
  148. sentry_sdk/integrations/spark/spark_worker.py +42 -50
  149. sentry_sdk/integrations/sqlalchemy.py +82 -37
  150. sentry_sdk/integrations/starlette.py +737 -0
  151. sentry_sdk/integrations/starlite.py +292 -0
  152. sentry_sdk/integrations/statsig.py +37 -0
  153. sentry_sdk/integrations/stdlib.py +100 -58
  154. sentry_sdk/integrations/strawberry.py +394 -0
  155. sentry_sdk/integrations/sys_exit.py +70 -0
  156. sentry_sdk/integrations/threading.py +142 -38
  157. sentry_sdk/integrations/tornado.py +68 -53
  158. sentry_sdk/integrations/trytond.py +15 -20
  159. sentry_sdk/integrations/typer.py +60 -0
  160. sentry_sdk/integrations/unleash.py +33 -0
  161. sentry_sdk/integrations/unraisablehook.py +53 -0
  162. sentry_sdk/integrations/wsgi.py +126 -125
  163. sentry_sdk/logger.py +96 -0
  164. sentry_sdk/metrics.py +81 -0
  165. sentry_sdk/monitor.py +120 -0
  166. sentry_sdk/profiler/__init__.py +49 -0
  167. sentry_sdk/profiler/continuous_profiler.py +730 -0
  168. sentry_sdk/profiler/transaction_profiler.py +839 -0
  169. sentry_sdk/profiler/utils.py +195 -0
  170. sentry_sdk/scope.py +1542 -112
  171. sentry_sdk/scrubber.py +177 -0
  172. sentry_sdk/serializer.py +152 -210
  173. sentry_sdk/session.py +177 -0
  174. sentry_sdk/sessions.py +202 -179
  175. sentry_sdk/spotlight.py +242 -0
  176. sentry_sdk/tracing.py +1202 -294
  177. sentry_sdk/tracing_utils.py +1236 -0
  178. sentry_sdk/transport.py +693 -189
  179. sentry_sdk/types.py +52 -0
  180. sentry_sdk/utils.py +1395 -228
  181. sentry_sdk/worker.py +30 -17
  182. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  183. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  184. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  185. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  186. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  187. sentry_sdk/_functools.py +0 -66
  188. sentry_sdk/integrations/celery.py +0 -275
  189. sentry_sdk/integrations/redis.py +0 -103
  190. sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.18.0.dist-info/METADATA +0 -66
  192. sentry_sdk-0.18.0.dist-info/RECORD +0 -65
  193. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/transport.py CHANGED
@@ -1,42 +1,64 @@
1
- from __future__ import print_function
2
-
1
+ from abc import ABC, abstractmethod
3
2
  import io
4
- import urllib3 # type: ignore
5
- import certifi
3
+ import os
6
4
  import gzip
5
+ import socket
6
+ import ssl
7
+ import time
8
+ import warnings
9
+ from datetime import datetime, timedelta, timezone
10
+ from collections import defaultdict
11
+ from urllib.request import getproxies
7
12
 
8
- from datetime import datetime, timedelta
13
+ try:
14
+ import brotli # type: ignore
15
+ except ImportError:
16
+ brotli = None
9
17
 
10
- from sentry_sdk.utils import Dsn, logger, capture_internal_exceptions, json_dumps
18
+ import urllib3
19
+ import certifi
20
+
21
+ import sentry_sdk
22
+ from sentry_sdk.consts import EndpointType
23
+ from sentry_sdk.utils import Dsn, logger, capture_internal_exceptions
11
24
  from sentry_sdk.worker import BackgroundWorker
12
- from sentry_sdk.envelope import Envelope
25
+ from sentry_sdk.envelope import Envelope, Item, PayloadRef
13
26
 
14
- from sentry_sdk._types import MYPY
27
+ from typing import TYPE_CHECKING, cast, List, Dict
15
28
 
16
- if MYPY:
29
+ if TYPE_CHECKING:
17
30
  from typing import Any
18
31
  from typing import Callable
19
- from typing import Dict
32
+ from typing import DefaultDict
20
33
  from typing import Iterable
34
+ from typing import Mapping
21
35
  from typing import Optional
36
+ from typing import Self
22
37
  from typing import Tuple
23
38
  from typing import Type
24
39
  from typing import Union
25
40
 
26
- from urllib3.poolmanager import PoolManager # type: ignore
41
+ from urllib3.poolmanager import PoolManager
27
42
  from urllib3.poolmanager import ProxyManager
28
43
 
29
- from sentry_sdk._types import Event, EndpointType
30
-
31
- DataCategory = Optional[str]
32
-
33
- try:
34
- from urllib.request import getproxies
35
- except ImportError:
36
- from urllib import getproxies # type: ignore
44
+ from sentry_sdk._types import Event, EventDataCategory
45
+
46
+ KEEP_ALIVE_SOCKET_OPTIONS = []
47
+ for option in [
48
+ (socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009
49
+ (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009
50
+ (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009
51
+ (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009
52
+ ]:
53
+ try:
54
+ KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2]))
55
+ except AttributeError:
56
+ # a specific option might not be available on specific systems,
57
+ # e.g. TCP_KEEPIDLE doesn't exist on macOS
58
+ pass
37
59
 
38
60
 
39
- class Transport(object):
61
+ class Transport(ABC):
40
62
  """Baseclass for all transports.
41
63
 
42
64
  A transport is used to send an event to sentry.
@@ -44,212 +66,396 @@ class Transport(object):
44
66
 
45
67
  parsed_dsn = None # type: Optional[Dsn]
46
68
 
47
- def __init__(
48
- self, options=None # type: Optional[Dict[str, Any]]
49
- ):
50
- # type: (...) -> None
69
+ def __init__(self, options=None):
70
+ # type: (Self, Optional[Dict[str, Any]]) -> None
51
71
  self.options = options
52
72
  if options and options["dsn"] is not None and options["dsn"]:
53
73
  self.parsed_dsn = Dsn(options["dsn"])
54
74
  else:
55
75
  self.parsed_dsn = None
56
76
 
57
- def capture_event(
58
- self, event # type: Event
59
- ):
60
- # type: (...) -> None
77
+ def capture_event(self, event):
78
+ # type: (Self, Event) -> None
61
79
  """
80
+ DEPRECATED: Please use capture_envelope instead.
81
+
62
82
  This gets invoked with the event dictionary when an event should
63
83
  be sent to sentry.
64
84
  """
65
- raise NotImplementedError()
66
85
 
67
- def capture_envelope(
68
- self, envelope # type: Envelope
69
- ):
70
- # type: (...) -> None
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
+ @abstractmethod
97
+ def capture_envelope(self, envelope):
98
+ # type: (Self, Envelope) -> None
71
99
  """
72
100
  Send an envelope to Sentry.
73
101
 
74
102
  Envelopes are a data container format that can hold any type of data
75
- submitted to Sentry. We use it for transactions and sessions, but
76
- regular "error" events should go through `capture_event` for backwards
77
- compat.
103
+ submitted to Sentry. We use it to send all event data (including errors,
104
+ transactions, crons check-ins, etc.) to Sentry.
78
105
  """
79
- raise NotImplementedError()
106
+ pass
80
107
 
81
108
  def flush(
82
109
  self,
83
- timeout, # type: float
84
- callback=None, # type: Optional[Any]
110
+ timeout,
111
+ callback=None,
85
112
  ):
86
- # type: (...) -> None
87
- """Wait `timeout` seconds for the current events to be sent out."""
88
- pass
113
+ # type: (Self, float, Optional[Any]) -> None
114
+ """
115
+ Wait `timeout` seconds for the current events to be sent out.
116
+
117
+ The default implementation is a no-op, since this method may only be relevant to some transports.
118
+ Subclasses should override this method if necessary.
119
+ """
120
+ return None
89
121
 
90
122
  def kill(self):
91
- # type: () -> None
92
- """Forcefully kills the transport."""
93
- pass
123
+ # type: (Self) -> None
124
+ """
125
+ Forcefully kills the transport.
94
126
 
95
- def __del__(self):
96
- # type: () -> None
97
- try:
98
- self.kill()
99
- except Exception:
100
- pass
127
+ The default implementation is a no-op, since this method may only be relevant to some transports.
128
+ Subclasses should override this method if necessary.
129
+ """
130
+ return None
131
+
132
+ def record_lost_event(
133
+ self,
134
+ reason, # type: str
135
+ data_category=None, # type: Optional[EventDataCategory]
136
+ item=None, # type: Optional[Item]
137
+ *,
138
+ quantity=1, # type: int
139
+ ):
140
+ # type: (...) -> None
141
+ """This increments a counter for event loss by reason and
142
+ data category by the given positive-int quantity (default 1).
143
+
144
+ If an item is provided, the data category and quantity are
145
+ extracted from the item, and the values passed for
146
+ data_category and quantity are ignored.
147
+
148
+ When recording a lost transaction via data_category="transaction",
149
+ the calling code should also record the lost spans via this method.
150
+ When recording lost spans, `quantity` should be set to the number
151
+ of contained spans, plus one for the transaction itself. When
152
+ passing an Item containing a transaction via the `item` parameter,
153
+ this method automatically records the lost spans.
154
+ """
155
+ return None
156
+
157
+ def is_healthy(self):
158
+ # type: (Self) -> bool
159
+ return True
101
160
 
102
161
 
103
162
  def _parse_rate_limits(header, now=None):
104
- # type: (Any, Optional[datetime]) -> Iterable[Tuple[DataCategory, datetime]]
163
+ # type: (str, Optional[datetime]) -> Iterable[Tuple[Optional[EventDataCategory], datetime]]
105
164
  if now is None:
106
- now = datetime.utcnow()
165
+ now = datetime.now(timezone.utc)
107
166
 
108
167
  for limit in header.split(","):
109
168
  try:
110
- retry_after, categories, _ = limit.strip().split(":", 2)
111
- retry_after = now + timedelta(seconds=int(retry_after))
169
+ parameters = limit.strip().split(":")
170
+ retry_after_val, categories = parameters[:2]
171
+
172
+ retry_after = now + timedelta(seconds=int(retry_after_val))
112
173
  for category in categories and categories.split(";") or (None,):
113
- yield category, retry_after
174
+ yield category, retry_after # type: ignore
114
175
  except (LookupError, ValueError):
115
176
  continue
116
177
 
117
178
 
118
- class HttpTransport(Transport):
119
- """The default HTTP transport."""
179
+ class BaseHttpTransport(Transport):
180
+ """The base HTTP transport."""
120
181
 
121
- def __init__(
122
- self, options # type: Dict[str, Any]
123
- ):
124
- # type: (...) -> None
182
+ TIMEOUT = 30 # seconds
183
+
184
+ def __init__(self, options):
185
+ # type: (Self, Dict[str, Any]) -> None
125
186
  from sentry_sdk.consts import VERSION
126
187
 
127
188
  Transport.__init__(self, options)
128
189
  assert self.parsed_dsn is not None
129
- self._worker = BackgroundWorker()
190
+ self.options = options # type: Dict[str, Any]
191
+ self._worker = BackgroundWorker(queue_size=options["transport_queue_size"])
130
192
  self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION)
131
- self._disabled_until = {} # type: Dict[DataCategory, datetime]
193
+ self._disabled_until = {} # type: Dict[Optional[EventDataCategory], datetime]
194
+ # We only use this Retry() class for the `get_retry_after` method it exposes
132
195
  self._retry = urllib3.util.Retry()
133
- self.options = options
196
+ self._discarded_events = defaultdict(int) # type: DefaultDict[Tuple[EventDataCategory, str], int]
197
+ self._last_client_report_sent = time.time()
134
198
 
135
- self._pool = self._make_pool(
136
- self.parsed_dsn,
137
- http_proxy=options["http_proxy"],
138
- https_proxy=options["https_proxy"],
139
- ca_certs=options["ca_certs"],
199
+ self._pool = self._make_pool()
200
+
201
+ # Backwards compatibility for deprecated `self.hub_class` attribute
202
+ self._hub_cls = sentry_sdk.Hub
203
+
204
+ experiments = options.get("_experiments", {})
205
+ compression_level = experiments.get(
206
+ "transport_compression_level",
207
+ experiments.get("transport_zlib_compression_level"),
140
208
  )
209
+ compression_algo = experiments.get(
210
+ "transport_compression_algo",
211
+ (
212
+ "gzip"
213
+ # if only compression level is set, assume gzip for backwards compatibility
214
+ # if we don't have brotli available, fallback to gzip
215
+ if compression_level is not None or brotli is None
216
+ else "br"
217
+ ),
218
+ )
219
+
220
+ if compression_algo == "br" and brotli is None:
221
+ logger.warning(
222
+ "You asked for brotli compression without the Brotli module, falling back to gzip -9"
223
+ )
224
+ compression_algo = "gzip"
225
+ compression_level = None
226
+
227
+ if compression_algo not in ("br", "gzip"):
228
+ logger.warning(
229
+ "Unknown compression algo %s, disabling compression", compression_algo
230
+ )
231
+ self._compression_level = 0
232
+ self._compression_algo = None
233
+ else:
234
+ self._compression_algo = compression_algo
141
235
 
142
- from sentry_sdk import Hub
236
+ if compression_level is not None:
237
+ self._compression_level = compression_level
238
+ elif self._compression_algo == "gzip":
239
+ self._compression_level = 9
240
+ elif self._compression_algo == "br":
241
+ self._compression_level = 4
143
242
 
144
- self.hub_cls = Hub
243
+ def record_lost_event(
244
+ self,
245
+ reason, # type: str
246
+ data_category=None, # type: Optional[EventDataCategory]
247
+ item=None, # type: Optional[Item]
248
+ *,
249
+ quantity=1, # type: int
250
+ ):
251
+ # type: (...) -> None
252
+ if not self.options["send_client_reports"]:
253
+ return
254
+
255
+ if item is not None:
256
+ data_category = item.data_category
257
+ quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below).
258
+
259
+ if data_category == "transaction":
260
+ # Also record the lost spans
261
+ event = item.get_transaction_event() or {}
262
+
263
+ # +1 for the transaction itself
264
+ span_count = (
265
+ len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1
266
+ )
267
+ self.record_lost_event(reason, "span", quantity=span_count)
268
+
269
+ elif data_category == "attachment":
270
+ # quantity of 0 is actually 1 as we do not want to count
271
+ # empty attachments as actually empty.
272
+ quantity = len(item.get_bytes()) or 1
273
+
274
+ elif data_category is None:
275
+ raise TypeError("data category not provided")
276
+
277
+ self._discarded_events[data_category, reason] += quantity
278
+
279
+ def _get_header_value(self, response, header):
280
+ # type: (Self, Any, str) -> Optional[str]
281
+ return response.headers.get(header)
145
282
 
146
283
  def _update_rate_limits(self, response):
147
- # type: (urllib3.HTTPResponse) -> None
284
+ # type: (Self, Union[urllib3.BaseHTTPResponse, httpcore.Response]) -> None
148
285
 
149
286
  # new sentries with more rate limit insights. We honor this header
150
287
  # no matter of the status code to update our internal rate limits.
151
- header = response.headers.get("x-sentry-rate-limits")
288
+ header = self._get_header_value(response, "x-sentry-rate-limits")
152
289
  if header:
290
+ logger.warning("Rate-limited via x-sentry-rate-limits")
153
291
  self._disabled_until.update(_parse_rate_limits(header))
154
292
 
155
293
  # old sentries only communicate global rate limit hits via the
156
294
  # retry-after header on 429. This header can also be emitted on new
157
295
  # sentries if a proxy in front wants to globally slow things down.
158
296
  elif response.status == 429:
159
- self._disabled_until[None] = datetime.utcnow() + timedelta(
160
- seconds=self._retry.get_retry_after(response) or 60
297
+ logger.warning("Rate-limited via 429")
298
+ retry_after_value = self._get_header_value(response, "Retry-After")
299
+ retry_after = (
300
+ self._retry.parse_retry_after(retry_after_value)
301
+ if retry_after_value is not None
302
+ else None
303
+ ) or 60
304
+ self._disabled_until[None] = datetime.now(timezone.utc) + timedelta(
305
+ seconds=retry_after
161
306
  )
162
307
 
163
308
  def _send_request(
164
309
  self,
165
- body, # type: bytes
166
- headers, # type: Dict[str, str]
167
- endpoint_type="store", # type: EndpointType
310
+ body,
311
+ headers,
312
+ endpoint_type=EndpointType.ENVELOPE,
313
+ envelope=None,
168
314
  ):
169
- # type: (...) -> None
315
+ # type: (Self, bytes, Dict[str, str], EndpointType, Optional[Envelope]) -> None
316
+
317
+ def record_loss(reason):
318
+ # type: (str) -> None
319
+ if envelope is None:
320
+ self.record_lost_event(reason, data_category="error")
321
+ else:
322
+ for item in envelope.items:
323
+ self.record_lost_event(reason, item=item)
324
+
170
325
  headers.update(
171
326
  {
172
327
  "User-Agent": str(self._auth.client),
173
328
  "X-Sentry-Auth": str(self._auth.to_header()),
174
329
  }
175
330
  )
176
- response = self._pool.request(
177
- "POST",
178
- str(self._auth.get_api_url(endpoint_type)),
179
- body=body,
180
- headers=headers,
181
- )
331
+ try:
332
+ response = self._request(
333
+ "POST",
334
+ endpoint_type,
335
+ body,
336
+ headers,
337
+ )
338
+ except Exception:
339
+ self.on_dropped_event("network")
340
+ record_loss("network_error")
341
+ raise
182
342
 
183
343
  try:
184
344
  self._update_rate_limits(response)
185
345
 
186
346
  if response.status == 429:
187
347
  # if we hit a 429. Something was rate limited but we already
188
- # acted on this in `self._update_rate_limits`.
348
+ # acted on this in `self._update_rate_limits`. Note that we
349
+ # do not want to record event loss here as we will have recorded
350
+ # an outcome in relay already.
351
+ self.on_dropped_event("status_429")
189
352
  pass
190
353
 
191
354
  elif response.status >= 300 or response.status < 200:
192
355
  logger.error(
193
356
  "Unexpected status code: %s (body: %s)",
194
357
  response.status,
195
- response.data,
358
+ getattr(response, "data", getattr(response, "content", None)),
196
359
  )
360
+ self.on_dropped_event("status_{}".format(response.status))
361
+ record_loss("network_error")
197
362
  finally:
198
363
  response.close()
199
364
 
365
+ def on_dropped_event(self, _reason):
366
+ # type: (Self, str) -> None
367
+ return None
368
+
369
+ def _fetch_pending_client_report(self, force=False, interval=60):
370
+ # type: (Self, bool, int) -> Optional[Item]
371
+ if not self.options["send_client_reports"]:
372
+ return None
373
+
374
+ if not (force or self._last_client_report_sent < time.time() - interval):
375
+ return None
376
+
377
+ discarded_events = self._discarded_events
378
+ self._discarded_events = defaultdict(int)
379
+ self._last_client_report_sent = time.time()
380
+
381
+ if not discarded_events:
382
+ return None
383
+
384
+ return Item(
385
+ PayloadRef(
386
+ json={
387
+ "timestamp": time.time(),
388
+ "discarded_events": [
389
+ {"reason": reason, "category": category, "quantity": quantity}
390
+ for (
391
+ (category, reason),
392
+ quantity,
393
+ ) in discarded_events.items()
394
+ ],
395
+ }
396
+ ),
397
+ type="client_report",
398
+ )
399
+
400
+ def _flush_client_reports(self, force=False):
401
+ # type: (Self, bool) -> None
402
+ client_report = self._fetch_pending_client_report(force=force, interval=60)
403
+ if client_report is not None:
404
+ self.capture_envelope(Envelope(items=[client_report]))
405
+
200
406
  def _check_disabled(self, category):
201
407
  # type: (str) -> bool
202
408
  def _disabled(bucket):
203
409
  # type: (Any) -> bool
204
410
  ts = self._disabled_until.get(bucket)
205
- return ts is not None and ts > datetime.utcnow()
411
+ return ts is not None and ts > datetime.now(timezone.utc)
206
412
 
207
413
  return _disabled(category) or _disabled(None)
208
414
 
209
- def _send_event(
210
- self, event # type: Event
211
- ):
212
- # type: (...) -> None
415
+ def _is_rate_limited(self):
416
+ # type: (Self) -> bool
417
+ return any(
418
+ ts > datetime.now(timezone.utc) for ts in self._disabled_until.values()
419
+ )
213
420
 
214
- if self._check_disabled("error"):
215
- return None
421
+ def _is_worker_full(self):
422
+ # type: (Self) -> bool
423
+ return self._worker.full()
216
424
 
217
- body = io.BytesIO()
218
- with gzip.GzipFile(fileobj=body, mode="w") as f:
219
- f.write(json_dumps(event))
425
+ def is_healthy(self):
426
+ # type: (Self) -> bool
427
+ return not (self._is_worker_full() or self._is_rate_limited())
220
428
 
221
- assert self.parsed_dsn is not None
222
- logger.debug(
223
- "Sending event, type:%s level:%s event_id:%s project:%s host:%s"
224
- % (
225
- event.get("type") or "null",
226
- event.get("level") or "null",
227
- event.get("event_id") or "null",
228
- self.parsed_dsn.project_id,
229
- self.parsed_dsn.host,
230
- )
231
- )
232
- self._send_request(
233
- body.getvalue(),
234
- headers={"Content-Type": "application/json", "Content-Encoding": "gzip"},
235
- )
236
- return None
237
-
238
- def _send_envelope(
239
- self, envelope # type: Envelope
240
- ):
241
- # type: (...) -> None
429
+ def _send_envelope(self, envelope):
430
+ # type: (Self, Envelope) -> None
242
431
 
243
432
  # remove all items from the envelope which are over quota
244
- envelope.items[:] = [
245
- x for x in envelope.items if not self._check_disabled(x.data_category)
246
- ]
433
+ new_items = []
434
+ for item in envelope.items:
435
+ if self._check_disabled(item.data_category):
436
+ if item.data_category in ("transaction", "error", "default", "statsd"):
437
+ self.on_dropped_event("self_rate_limits")
438
+ self.record_lost_event("ratelimit_backoff", item=item)
439
+ else:
440
+ new_items.append(item)
441
+
442
+ # Since we're modifying the envelope here make a copy so that others
443
+ # that hold references do not see their envelope modified.
444
+ envelope = Envelope(headers=envelope.headers, items=new_items)
445
+
247
446
  if not envelope.items:
248
447
  return None
249
448
 
250
- body = io.BytesIO()
251
- with gzip.GzipFile(fileobj=body, mode="w") as f:
252
- envelope.serialize_into(f)
449
+ # since we're already in the business of sending out an envelope here
450
+ # check if we have one pending for the stats session envelopes so we
451
+ # can attach it to this enveloped scheduled for sending. This will
452
+ # currently typically attach the client report to the most recent
453
+ # session update.
454
+ client_report_item = self._fetch_pending_client_report(interval=30)
455
+ if client_report_item is not None:
456
+ envelope.items.append(client_report_item)
457
+
458
+ content_encoding, body = self._serialize_envelope(envelope)
253
459
 
254
460
  assert self.parsed_dsn is not None
255
461
  logger.debug(
@@ -258,26 +464,49 @@ class HttpTransport(Transport):
258
464
  self.parsed_dsn.project_id,
259
465
  self.parsed_dsn.host,
260
466
  )
467
+
468
+ headers = {
469
+ "Content-Type": "application/x-sentry-envelope",
470
+ }
471
+ if content_encoding:
472
+ headers["Content-Encoding"] = content_encoding
473
+
261
474
  self._send_request(
262
475
  body.getvalue(),
263
- headers={
264
- "Content-Type": "application/x-sentry-envelope",
265
- "Content-Encoding": "gzip",
266
- },
267
- endpoint_type="envelope",
476
+ headers=headers,
477
+ endpoint_type=EndpointType.ENVELOPE,
478
+ envelope=envelope,
268
479
  )
269
480
  return None
270
481
 
271
- def _get_pool_options(self, ca_certs):
272
- # type: (Optional[Any]) -> Dict[str, Any]
273
- return {
274
- "num_pools": 2,
275
- "cert_reqs": "CERT_REQUIRED",
276
- "ca_certs": ca_certs or certifi.where(),
277
- }
482
+ def _serialize_envelope(self, envelope):
483
+ # type: (Self, Envelope) -> tuple[Optional[str], io.BytesIO]
484
+ content_encoding = None
485
+ body = io.BytesIO()
486
+ if self._compression_level == 0 or self._compression_algo is None:
487
+ envelope.serialize_into(body)
488
+ else:
489
+ content_encoding = self._compression_algo
490
+ if self._compression_algo == "br" and brotli is not None:
491
+ body.write(
492
+ brotli.compress(
493
+ envelope.serialize(), quality=self._compression_level
494
+ )
495
+ )
496
+ else: # assume gzip as we sanitize the algo value in init
497
+ with gzip.GzipFile(
498
+ fileobj=body, mode="w", compresslevel=self._compression_level
499
+ ) as f:
500
+ envelope.serialize_into(f)
501
+
502
+ return content_encoding, body
503
+
504
+ def _get_pool_options(self):
505
+ # type: (Self) -> Dict[str, Any]
506
+ raise NotImplementedError()
278
507
 
279
508
  def _in_no_proxy(self, parsed_dsn):
280
- # type: (Dsn) -> bool
509
+ # type: (Self, Dsn) -> bool
281
510
  no_proxy = getproxies().get("no")
282
511
  if not no_proxy:
283
512
  return False
@@ -287,105 +516,380 @@ class HttpTransport(Transport):
287
516
  return True
288
517
  return False
289
518
 
290
- def _make_pool(
519
+ def _make_pool(self):
520
+ # type: (Self) -> Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]
521
+ raise NotImplementedError()
522
+
523
+ def _request(
524
+ self,
525
+ method,
526
+ endpoint_type,
527
+ body,
528
+ headers,
529
+ ):
530
+ # type: (Self, str, EndpointType, Any, Mapping[str, str]) -> Union[urllib3.BaseHTTPResponse, httpcore.Response]
531
+ raise NotImplementedError()
532
+
533
+ def capture_envelope(
534
+ self,
535
+ envelope, # type: Envelope
536
+ ):
537
+ # type: (...) -> None
538
+ def send_envelope_wrapper():
539
+ # type: () -> None
540
+ with capture_internal_exceptions():
541
+ self._send_envelope(envelope)
542
+ self._flush_client_reports()
543
+
544
+ if not self._worker.submit(send_envelope_wrapper):
545
+ self.on_dropped_event("full_queue")
546
+ for item in envelope.items:
547
+ self.record_lost_event("queue_overflow", item=item)
548
+
549
+ def flush(
291
550
  self,
292
- parsed_dsn, # type: Dsn
293
- http_proxy, # type: Optional[str]
294
- https_proxy, # type: Optional[str]
295
- ca_certs, # type: Optional[Any]
551
+ timeout,
552
+ callback=None,
296
553
  ):
297
- # type: (...) -> Union[PoolManager, ProxyManager]
554
+ # type: (Self, float, Optional[Callable[[int, float], None]]) -> None
555
+ logger.debug("Flushing HTTP transport")
556
+
557
+ if timeout > 0:
558
+ self._worker.submit(lambda: self._flush_client_reports(force=True))
559
+ self._worker.flush(timeout, callback)
560
+
561
+ def kill(self):
562
+ # type: (Self) -> None
563
+ logger.debug("Killing HTTP transport")
564
+ self._worker.kill()
565
+
566
+ @staticmethod
567
+ def _warn_hub_cls():
568
+ # type: () -> None
569
+ """Convenience method to warn users about the deprecation of the `hub_cls` attribute."""
570
+ warnings.warn(
571
+ "The `hub_cls` attribute is deprecated and will be removed in a future release.",
572
+ DeprecationWarning,
573
+ stacklevel=3,
574
+ )
575
+
576
+ @property
577
+ def hub_cls(self):
578
+ # type: (Self) -> type[sentry_sdk.Hub]
579
+ """DEPRECATED: This attribute is deprecated and will be removed in a future release."""
580
+ HttpTransport._warn_hub_cls()
581
+ return self._hub_cls
582
+
583
+ @hub_cls.setter
584
+ def hub_cls(self, value):
585
+ # type: (Self, type[sentry_sdk.Hub]) -> None
586
+ """DEPRECATED: This attribute is deprecated and will be removed in a future release."""
587
+ HttpTransport._warn_hub_cls()
588
+ self._hub_cls = value
589
+
590
+
591
+ class HttpTransport(BaseHttpTransport):
592
+ if TYPE_CHECKING:
593
+ _pool: Union[PoolManager, ProxyManager]
594
+
595
+ def _get_pool_options(self):
596
+ # type: (Self) -> Dict[str, Any]
597
+
598
+ num_pools = self.options.get("_experiments", {}).get("transport_num_pools")
599
+ options = {
600
+ "num_pools": 2 if num_pools is None else int(num_pools),
601
+ "cert_reqs": "CERT_REQUIRED",
602
+ "timeout": urllib3.Timeout(total=self.TIMEOUT),
603
+ }
604
+
605
+ socket_options = None # type: Optional[List[Tuple[int, int, int | bytes]]]
606
+
607
+ if self.options["socket_options"] is not None:
608
+ socket_options = self.options["socket_options"]
609
+
610
+ if self.options["keep_alive"]:
611
+ if socket_options is None:
612
+ socket_options = []
613
+
614
+ used_options = {(o[0], o[1]) for o in socket_options}
615
+ for default_option in KEEP_ALIVE_SOCKET_OPTIONS:
616
+ if (default_option[0], default_option[1]) not in used_options:
617
+ socket_options.append(default_option)
618
+
619
+ if socket_options is not None:
620
+ options["socket_options"] = socket_options
621
+
622
+ options["ca_certs"] = (
623
+ self.options["ca_certs"] # User-provided bundle from the SDK init
624
+ or os.environ.get("SSL_CERT_FILE")
625
+ or os.environ.get("REQUESTS_CA_BUNDLE")
626
+ or certifi.where()
627
+ )
628
+
629
+ options["cert_file"] = self.options["cert_file"] or os.environ.get(
630
+ "CLIENT_CERT_FILE"
631
+ )
632
+ options["key_file"] = self.options["key_file"] or os.environ.get(
633
+ "CLIENT_KEY_FILE"
634
+ )
635
+
636
+ return options
637
+
638
+ def _make_pool(self):
639
+ # type: (Self) -> Union[PoolManager, ProxyManager]
640
+ if self.parsed_dsn is None:
641
+ raise ValueError("Cannot create HTTP-based transport without valid DSN")
642
+
298
643
  proxy = None
299
- no_proxy = self._in_no_proxy(parsed_dsn)
644
+ no_proxy = self._in_no_proxy(self.parsed_dsn)
300
645
 
301
646
  # try HTTPS first
302
- if parsed_dsn.scheme == "https" and (https_proxy != ""):
647
+ https_proxy = self.options["https_proxy"]
648
+ if self.parsed_dsn.scheme == "https" and (https_proxy != ""):
303
649
  proxy = https_proxy or (not no_proxy and getproxies().get("https"))
304
650
 
305
651
  # maybe fallback to HTTP proxy
652
+ http_proxy = self.options["http_proxy"]
306
653
  if not proxy and (http_proxy != ""):
307
654
  proxy = http_proxy or (not no_proxy and getproxies().get("http"))
308
655
 
309
- opts = self._get_pool_options(ca_certs)
656
+ opts = self._get_pool_options()
310
657
 
311
658
  if proxy:
312
- return urllib3.ProxyManager(proxy, **opts)
659
+ proxy_headers = self.options["proxy_headers"]
660
+ if proxy_headers:
661
+ opts["proxy_headers"] = proxy_headers
662
+
663
+ if proxy.startswith("socks"):
664
+ use_socks_proxy = True
665
+ try:
666
+ # Check if PySocks dependency is available
667
+ from urllib3.contrib.socks import SOCKSProxyManager
668
+ except ImportError:
669
+ use_socks_proxy = False
670
+ logger.warning(
671
+ "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.",
672
+ proxy,
673
+ )
674
+
675
+ if use_socks_proxy:
676
+ return SOCKSProxyManager(proxy, **opts)
677
+ else:
678
+ return urllib3.PoolManager(**opts)
679
+ else:
680
+ return urllib3.ProxyManager(proxy, **opts)
313
681
  else:
314
682
  return urllib3.PoolManager(**opts)
315
683
 
316
- def capture_event(
317
- self, event # type: Event
684
+ def _request(
685
+ self,
686
+ method,
687
+ endpoint_type,
688
+ body,
689
+ headers,
318
690
  ):
319
- # type: (...) -> None
320
- hub = self.hub_cls.current
691
+ # type: (Self, str, EndpointType, Any, Mapping[str, str]) -> urllib3.BaseHTTPResponse
692
+ return self._pool.request(
693
+ method,
694
+ self._auth.get_api_url(endpoint_type),
695
+ body=body,
696
+ headers=headers,
697
+ )
321
698
 
322
- def send_event_wrapper():
323
- # type: () -> None
324
- with hub:
325
- with capture_internal_exceptions():
326
- self._send_event(event)
327
699
 
328
- self._worker.submit(send_event_wrapper)
700
+ try:
701
+ import httpcore
702
+ import h2 # noqa: F401
703
+ except ImportError:
704
+ # Sorry, no Http2Transport for you
705
+ class Http2Transport(HttpTransport):
706
+ def __init__(self, options):
707
+ # type: (Self, Dict[str, Any]) -> None
708
+ super().__init__(options)
709
+ logger.warning(
710
+ "You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport."
711
+ )
329
712
 
330
- def capture_envelope(
331
- self, envelope # type: Envelope
332
- ):
333
- # type: (...) -> None
334
- hub = self.hub_cls.current
713
+ else:
335
714
 
336
- def send_envelope_wrapper():
337
- # type: () -> None
338
- with hub:
339
- with capture_internal_exceptions():
340
- self._send_envelope(envelope)
715
+ class Http2Transport(BaseHttpTransport): # type: ignore
716
+ """The HTTP2 transport based on httpcore."""
341
717
 
342
- self._worker.submit(send_envelope_wrapper)
718
+ TIMEOUT = 15
343
719
 
344
- def flush(
345
- self,
346
- timeout, # type: float
347
- callback=None, # type: Optional[Any]
348
- ):
349
- # type: (...) -> None
350
- logger.debug("Flushing HTTP transport")
351
- if timeout > 0:
352
- self._worker.flush(timeout, callback)
720
+ if TYPE_CHECKING:
721
+ _pool: Union[
722
+ httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool
723
+ ]
353
724
 
354
- def kill(self):
355
- # type: () -> None
356
- logger.debug("Killing HTTP transport")
357
- self._worker.kill()
725
+ def _get_header_value(self, response, header):
726
+ # type: (Self, httpcore.Response, str) -> Optional[str]
727
+ return next(
728
+ (
729
+ val.decode("ascii")
730
+ for key, val in response.headers
731
+ if key.decode("ascii").lower() == header
732
+ ),
733
+ None,
734
+ )
735
+
736
+ def _request(
737
+ self,
738
+ method,
739
+ endpoint_type,
740
+ body,
741
+ headers,
742
+ ):
743
+ # type: (Self, str, EndpointType, Any, Mapping[str, str]) -> httpcore.Response
744
+ response = self._pool.request(
745
+ method,
746
+ self._auth.get_api_url(endpoint_type),
747
+ content=body,
748
+ headers=headers, # type: ignore
749
+ extensions={
750
+ "timeout": {
751
+ "pool": self.TIMEOUT,
752
+ "connect": self.TIMEOUT,
753
+ "write": self.TIMEOUT,
754
+ "read": self.TIMEOUT,
755
+ }
756
+ },
757
+ )
758
+ return response
759
+
760
+ def _get_pool_options(self):
761
+ # type: (Self) -> Dict[str, Any]
762
+ options = {
763
+ "http2": self.parsed_dsn is not None
764
+ and self.parsed_dsn.scheme == "https",
765
+ "retries": 3,
766
+ } # type: Dict[str, Any]
767
+
768
+ socket_options = (
769
+ self.options["socket_options"]
770
+ if self.options["socket_options"] is not None
771
+ else []
772
+ )
773
+
774
+ used_options = {(o[0], o[1]) for o in socket_options}
775
+ for default_option in KEEP_ALIVE_SOCKET_OPTIONS:
776
+ if (default_option[0], default_option[1]) not in used_options:
777
+ socket_options.append(default_option)
778
+
779
+ options["socket_options"] = socket_options
780
+
781
+ ssl_context = ssl.create_default_context()
782
+ ssl_context.load_verify_locations(
783
+ self.options["ca_certs"] # User-provided bundle from the SDK init
784
+ or os.environ.get("SSL_CERT_FILE")
785
+ or os.environ.get("REQUESTS_CA_BUNDLE")
786
+ or certifi.where()
787
+ )
788
+ cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE")
789
+ key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE")
790
+ if cert_file is not None:
791
+ ssl_context.load_cert_chain(cert_file, key_file)
792
+
793
+ options["ssl_context"] = ssl_context
794
+
795
+ return options
796
+
797
+ def _make_pool(self):
798
+ # type: (Self) -> Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]
799
+ if self.parsed_dsn is None:
800
+ raise ValueError("Cannot create HTTP-based transport without valid DSN")
801
+ proxy = None
802
+ no_proxy = self._in_no_proxy(self.parsed_dsn)
803
+
804
+ # try HTTPS first
805
+ https_proxy = self.options["https_proxy"]
806
+ if self.parsed_dsn.scheme == "https" and (https_proxy != ""):
807
+ proxy = https_proxy or (not no_proxy and getproxies().get("https"))
808
+
809
+ # maybe fallback to HTTP proxy
810
+ http_proxy = self.options["http_proxy"]
811
+ if not proxy and (http_proxy != ""):
812
+ proxy = http_proxy or (not no_proxy and getproxies().get("http"))
813
+
814
+ opts = self._get_pool_options()
815
+
816
+ if proxy:
817
+ proxy_headers = self.options["proxy_headers"]
818
+ if proxy_headers:
819
+ opts["proxy_headers"] = proxy_headers
820
+
821
+ if proxy.startswith("socks"):
822
+ try:
823
+ if "socket_options" in opts:
824
+ socket_options = opts.pop("socket_options")
825
+ if socket_options:
826
+ logger.warning(
827
+ "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options."
828
+ )
829
+ return httpcore.SOCKSProxy(proxy_url=proxy, **opts)
830
+ except RuntimeError:
831
+ logger.warning(
832
+ "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.",
833
+ proxy,
834
+ )
835
+ else:
836
+ return httpcore.HTTPProxy(proxy_url=proxy, **opts)
837
+
838
+ return httpcore.ConnectionPool(**opts)
358
839
 
359
840
 
360
841
  class _FunctionTransport(Transport):
842
+ """
843
+ DEPRECATED: Users wishing to provide a custom transport should subclass
844
+ the Transport class, rather than providing a function.
845
+ """
846
+
361
847
  def __init__(
362
- self, func # type: Callable[[Event], None]
848
+ self,
849
+ func, # type: Callable[[Event], None]
363
850
  ):
364
851
  # type: (...) -> None
365
852
  Transport.__init__(self)
366
853
  self._func = func
367
854
 
368
855
  def capture_event(
369
- self, event # type: Event
856
+ self,
857
+ event, # type: Event
370
858
  ):
371
859
  # type: (...) -> None
372
860
  self._func(event)
373
861
  return None
374
862
 
863
+ def capture_envelope(self, envelope: Envelope) -> None:
864
+ # Since function transports expect to be called with an event, we need
865
+ # to iterate over the envelope and call the function for each event, via
866
+ # the deprecated capture_event method.
867
+ event = envelope.get_event()
868
+ if event is not None:
869
+ self.capture_event(event)
870
+
375
871
 
376
872
  def make_transport(options):
377
873
  # type: (Dict[str, Any]) -> Optional[Transport]
378
874
  ref_transport = options["transport"]
379
875
 
380
- # If no transport is given, we use the http transport class
381
- if ref_transport is None:
382
- transport_cls = HttpTransport # type: Type[Transport]
383
- elif isinstance(ref_transport, Transport):
876
+ use_http2_transport = options.get("_experiments", {}).get("transport_http2", False)
877
+
878
+ # By default, we use the http transport class
879
+ transport_cls = Http2Transport if use_http2_transport else HttpTransport # type: Type[Transport]
880
+
881
+ if isinstance(ref_transport, Transport):
384
882
  return ref_transport
385
883
  elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport):
386
884
  transport_cls = ref_transport
387
885
  elif callable(ref_transport):
388
- return _FunctionTransport(ref_transport) # type: ignore
886
+ warnings.warn(
887
+ "Function transports are deprecated and will be removed in a future release."
888
+ "Please provide a Transport instance or subclass, instead.",
889
+ DeprecationWarning,
890
+ stacklevel=2,
891
+ )
892
+ return _FunctionTransport(ref_transport)
389
893
 
390
894
  # if a transport class is given only instantiate it if the dsn is not
391
895
  # empty or None