sentry-sdk 0.7.5__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 -30
  2. sentry_sdk/_compat.py +74 -61
  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 +289 -0
  8. sentry_sdk/_types.py +338 -0
  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 +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  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 +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  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 +134 -0
  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 +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  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 +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  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 +141 -0
  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 +112 -68
  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 +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/transport.py CHANGED
@@ -1,225 +1,897 @@
1
- from __future__ import print_function
2
-
3
- import json
1
+ from abc import ABC, abstractmethod
4
2
  import io
5
- import urllib3 # type: ignore
6
- import certifi
3
+ import os
7
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
12
+
13
+ try:
14
+ import brotli # type: ignore
15
+ except ImportError:
16
+ brotli = None
8
17
 
9
- from datetime import datetime, timedelta
18
+ import urllib3
19
+ import certifi
10
20
 
11
- from sentry_sdk.consts import VERSION
21
+ import sentry_sdk
22
+ from sentry_sdk.consts import EndpointType
12
23
  from sentry_sdk.utils import Dsn, logger, capture_internal_exceptions
13
24
  from sentry_sdk.worker import BackgroundWorker
25
+ from sentry_sdk.envelope import Envelope, Item, PayloadRef
14
26
 
15
- if False:
16
- from sentry_sdk.consts import ClientOptions
17
- from typing import Type
27
+ from typing import TYPE_CHECKING, cast, List, Dict
28
+
29
+ if TYPE_CHECKING:
18
30
  from typing import Any
31
+ from typing import Callable
32
+ from typing import DefaultDict
33
+ from typing import Iterable
34
+ from typing import Mapping
19
35
  from typing import Optional
20
- from typing import Dict
36
+ from typing import Self
37
+ from typing import Tuple
38
+ from typing import Type
21
39
  from typing import Union
22
- from typing import Callable
23
- from urllib3.poolmanager import PoolManager # type: ignore
24
- from urllib3.poolmanager import ProxyManager # type: ignore
25
40
 
26
- try:
27
- from urllib.request import getproxies
28
- except ImportError:
29
- from urllib import getproxies # type: ignore
41
+ from urllib3.poolmanager import PoolManager
42
+ from urllib3.poolmanager import ProxyManager
43
+
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
30
59
 
31
60
 
32
- class Transport(object):
61
+ class Transport(ABC):
33
62
  """Baseclass for all transports.
34
63
 
35
64
  A transport is used to send an event to sentry.
36
65
  """
37
66
 
38
- parsed_dsn = None # type: Dsn
67
+ parsed_dsn = None # type: Optional[Dsn]
39
68
 
40
69
  def __init__(self, options=None):
41
- # type: (Optional[ClientOptions]) -> None
70
+ # type: (Self, Optional[Dict[str, Any]]) -> None
42
71
  self.options = options
43
- if options and options["dsn"]:
72
+ if options and options["dsn"] is not None and options["dsn"]:
44
73
  self.parsed_dsn = Dsn(options["dsn"])
45
74
  else:
46
- self.parsed_dsn = None # type: ignore
75
+ self.parsed_dsn = None
47
76
 
48
77
  def capture_event(self, event):
49
- """This gets invoked with the event dictionary when an event should
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
50
83
  be sent to sentry.
51
84
  """
52
- raise NotImplementedError()
53
85
 
54
- def flush(self, timeout, callback=None):
55
- """Wait `timeout` seconds for the current events to be sent out."""
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
99
+ """
100
+ Send an envelope to Sentry.
101
+
102
+ Envelopes are a data container format that can hold any type of data
103
+ submitted to Sentry. We use it to send all event data (including errors,
104
+ transactions, crons check-ins, etc.) to Sentry.
105
+ """
56
106
  pass
57
107
 
108
+ def flush(
109
+ self,
110
+ timeout,
111
+ callback=None,
112
+ ):
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
121
+
58
122
  def kill(self):
59
- # type: () -> None
60
- """Forcefully kills the transport."""
61
- pass
123
+ # type: (Self) -> None
124
+ """
125
+ Forcefully kills the transport.
62
126
 
63
- def __del__(self):
64
- # type: () -> None
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
160
+
161
+
162
+ def _parse_rate_limits(header, now=None):
163
+ # type: (str, Optional[datetime]) -> Iterable[Tuple[Optional[EventDataCategory], datetime]]
164
+ if now is None:
165
+ now = datetime.now(timezone.utc)
166
+
167
+ for limit in header.split(","):
65
168
  try:
66
- self.kill()
67
- except Exception:
68
- pass
169
+ parameters = limit.strip().split(":")
170
+ retry_after_val, categories = parameters[:2]
69
171
 
172
+ retry_after = now + timedelta(seconds=int(retry_after_val))
173
+ for category in categories and categories.split(";") or (None,):
174
+ yield category, retry_after # type: ignore
175
+ except (LookupError, ValueError):
176
+ continue
70
177
 
71
- class HttpTransport(Transport):
72
- """The default HTTP transport."""
178
+
179
+ class BaseHttpTransport(Transport):
180
+ """The base HTTP transport."""
181
+
182
+ TIMEOUT = 30 # seconds
73
183
 
74
184
  def __init__(self, options):
75
- # type: (ClientOptions) -> None
185
+ # type: (Self, Dict[str, Any]) -> None
186
+ from sentry_sdk.consts import VERSION
187
+
76
188
  Transport.__init__(self, options)
77
- self._worker = BackgroundWorker()
189
+ assert self.parsed_dsn is not None
190
+ self.options = options # type: Dict[str, Any]
191
+ self._worker = BackgroundWorker(queue_size=options["transport_queue_size"])
78
192
  self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION)
79
- self._disabled_until = None # type: Optional[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
80
195
  self._retry = urllib3.util.Retry()
81
- self.options = options
196
+ self._discarded_events = defaultdict(int) # type: DefaultDict[Tuple[EventDataCategory, str], int]
197
+ self._last_client_report_sent = time.time()
198
+
199
+ self._pool = self._make_pool()
200
+
201
+ # Backwards compatibility for deprecated `self.hub_class` attribute
202
+ self._hub_cls = sentry_sdk.Hub
82
203
 
83
- self._pool = self._make_pool(
84
- self.parsed_dsn,
85
- http_proxy=options["http_proxy"],
86
- https_proxy=options["https_proxy"],
87
- ca_certs=options["ca_certs"],
204
+ experiments = options.get("_experiments", {})
205
+ compression_level = experiments.get(
206
+ "transport_compression_level",
207
+ experiments.get("transport_zlib_compression_level"),
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
+ ),
88
218
  )
89
219
 
90
- from sentry_sdk import Hub
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
91
226
 
92
- self.hub_cls = Hub
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
93
235
 
94
- def _send_event(self, event):
95
- # type: (Dict[str, Any]) -> None
96
- if self._disabled_until is not None:
97
- if datetime.utcnow() < self._disabled_until:
98
- return
99
- self._disabled_until = None
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
100
242
 
101
- body = io.BytesIO()
102
- with gzip.GzipFile(fileobj=body, mode="w") as f:
103
- f.write(json.dumps(event, allow_nan=False).encode("utf-8"))
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
104
254
 
105
- logger.debug(
106
- "Sending %s event [%s] to %s project:%s"
107
- % (
108
- event.get("level") or "error",
109
- event["event_id"],
110
- self.parsed_dsn.host,
111
- self.parsed_dsn.project_id,
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)
282
+
283
+ def _update_rate_limits(self, response):
284
+ # type: (Self, Union[urllib3.BaseHTTPResponse, httpcore.Response]) -> None
285
+
286
+ # new sentries with more rate limit insights. We honor this header
287
+ # no matter of the status code to update our internal rate limits.
288
+ header = self._get_header_value(response, "x-sentry-rate-limits")
289
+ if header:
290
+ logger.warning("Rate-limited via x-sentry-rate-limits")
291
+ self._disabled_until.update(_parse_rate_limits(header))
292
+
293
+ # old sentries only communicate global rate limit hits via the
294
+ # retry-after header on 429. This header can also be emitted on new
295
+ # sentries if a proxy in front wants to globally slow things down.
296
+ elif response.status == 429:
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
112
306
  )
113
- )
114
- response = self._pool.request(
115
- "POST",
116
- str(self._auth.store_api_url),
117
- body=body.getvalue(),
118
- headers={
307
+
308
+ def _send_request(
309
+ self,
310
+ body,
311
+ headers,
312
+ endpoint_type=EndpointType.ENVELOPE,
313
+ envelope=None,
314
+ ):
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
+
325
+ headers.update(
326
+ {
327
+ "User-Agent": str(self._auth.client),
119
328
  "X-Sentry-Auth": str(self._auth.to_header()),
120
- "Content-Type": "application/json",
121
- "Content-Encoding": "gzip",
122
- },
329
+ }
123
330
  )
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
124
342
 
125
343
  try:
344
+ self._update_rate_limits(response)
345
+
126
346
  if response.status == 429:
127
- self._disabled_until = datetime.utcnow() + timedelta(
128
- seconds=self._retry.get_retry_after(response) or 60
129
- )
130
- return
347
+ # if we hit a 429. Something was rate limited but we already
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")
352
+ pass
131
353
 
132
354
  elif response.status >= 300 or response.status < 200:
133
- raise ValueError("Unexpected status code: %s" % response.status)
355
+ logger.error(
356
+ "Unexpected status code: %s (body: %s)",
357
+ response.status,
358
+ getattr(response, "data", getattr(response, "content", None)),
359
+ )
360
+ self.on_dropped_event("status_{}".format(response.status))
361
+ record_loss("network_error")
134
362
  finally:
135
363
  response.close()
136
364
 
137
- self._disabled_until = None
365
+ def on_dropped_event(self, _reason):
366
+ # type: (Self, str) -> None
367
+ return None
138
368
 
139
- def _get_pool_options(self, ca_certs):
140
- # type: (Optional[Any]) -> Dict[str, Any]
141
- return {
142
- "num_pools": 2,
143
- "cert_reqs": "CERT_REQUIRED",
144
- "ca_certs": ca_certs or certifi.where(),
145
- }
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
+ )
146
399
 
147
- def _make_pool(
148
- self,
149
- parsed_dsn, # type: Dsn
150
- http_proxy, # type: Optional[str]
151
- https_proxy, # type: Optional[str]
152
- ca_certs, # type: Optional[Any]
153
- ):
154
- # type: (...) -> Union[PoolManager, ProxyManager]
155
- # Use http_proxy if scheme is https and https_proxy is not set
156
- proxy = parsed_dsn.scheme == "https" and https_proxy or http_proxy
157
- if not proxy:
158
- proxy = getproxies().get(parsed_dsn.scheme)
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
+
406
+ def _check_disabled(self, category):
407
+ # type: (str) -> bool
408
+ def _disabled(bucket):
409
+ # type: (Any) -> bool
410
+ ts = self._disabled_until.get(bucket)
411
+ return ts is not None and ts > datetime.now(timezone.utc)
412
+
413
+ return _disabled(category) or _disabled(None)
414
+
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
+ )
159
420
 
160
- opts = self._get_pool_options(ca_certs)
421
+ def _is_worker_full(self):
422
+ # type: (Self) -> bool
423
+ return self._worker.full()
424
+
425
+ def is_healthy(self):
426
+ # type: (Self) -> bool
427
+ return not (self._is_worker_full() or self._is_rate_limited())
428
+
429
+ def _send_envelope(self, envelope):
430
+ # type: (Self, Envelope) -> None
431
+
432
+ # remove all items from the envelope which are over quota
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
+
446
+ if not envelope.items:
447
+ return None
448
+
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)
459
+
460
+ assert self.parsed_dsn is not None
461
+ logger.debug(
462
+ "Sending envelope [%s] project:%s host:%s",
463
+ envelope.description,
464
+ self.parsed_dsn.project_id,
465
+ self.parsed_dsn.host,
466
+ )
161
467
 
162
- if proxy:
163
- return urllib3.ProxyManager(proxy, **opts)
468
+ headers = {
469
+ "Content-Type": "application/x-sentry-envelope",
470
+ }
471
+ if content_encoding:
472
+ headers["Content-Encoding"] = content_encoding
473
+
474
+ self._send_request(
475
+ body.getvalue(),
476
+ headers=headers,
477
+ endpoint_type=EndpointType.ENVELOPE,
478
+ envelope=envelope,
479
+ )
480
+ return None
481
+
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)
164
488
  else:
165
- return urllib3.PoolManager(**opts)
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)
166
501
 
167
- def capture_event(self, event):
168
- # type: (Dict[str, Any]) -> None
169
- hub = self.hub_cls.current
502
+ return content_encoding, body
503
+
504
+ def _get_pool_options(self):
505
+ # type: (Self) -> Dict[str, Any]
506
+ raise NotImplementedError()
507
+
508
+ def _in_no_proxy(self, parsed_dsn):
509
+ # type: (Self, Dsn) -> bool
510
+ no_proxy = getproxies().get("no")
511
+ if not no_proxy:
512
+ return False
513
+ for host in no_proxy.split(","):
514
+ host = host.strip()
515
+ if parsed_dsn.host.endswith(host) or parsed_dsn.netloc.endswith(host):
516
+ return True
517
+ return False
518
+
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()
170
532
 
171
- def send_event_wrapper():
533
+ def capture_envelope(
534
+ self,
535
+ envelope, # type: Envelope
536
+ ):
537
+ # type: (...) -> None
538
+ def send_envelope_wrapper():
172
539
  # type: () -> None
173
- with hub:
174
- with capture_internal_exceptions():
175
- self._send_event(event)
540
+ with capture_internal_exceptions():
541
+ self._send_envelope(envelope)
542
+ self._flush_client_reports()
176
543
 
177
- self._worker.submit(send_event_wrapper)
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)
178
548
 
179
- def flush(self, timeout, callback=None):
180
- # type: (float, Optional[Any]) -> None
549
+ def flush(
550
+ self,
551
+ timeout,
552
+ callback=None,
553
+ ):
554
+ # type: (Self, float, Optional[Callable[[int, float], None]]) -> None
181
555
  logger.debug("Flushing HTTP transport")
556
+
182
557
  if timeout > 0:
558
+ self._worker.submit(lambda: self._flush_client_reports(force=True))
183
559
  self._worker.flush(timeout, callback)
184
560
 
185
561
  def kill(self):
186
- # type: () -> None
562
+ # type: (Self) -> None
187
563
  logger.debug("Killing HTTP transport")
188
564
  self._worker.kill()
189
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
+
643
+ proxy = None
644
+ no_proxy = self._in_no_proxy(self.parsed_dsn)
645
+
646
+ # try HTTPS first
647
+ https_proxy = self.options["https_proxy"]
648
+ if self.parsed_dsn.scheme == "https" and (https_proxy != ""):
649
+ proxy = https_proxy or (not no_proxy and getproxies().get("https"))
650
+
651
+ # maybe fallback to HTTP proxy
652
+ http_proxy = self.options["http_proxy"]
653
+ if not proxy and (http_proxy != ""):
654
+ proxy = http_proxy or (not no_proxy and getproxies().get("http"))
655
+
656
+ opts = self._get_pool_options()
657
+
658
+ if proxy:
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)
681
+ else:
682
+ return urllib3.PoolManager(**opts)
683
+
684
+ def _request(
685
+ self,
686
+ method,
687
+ endpoint_type,
688
+ body,
689
+ headers,
690
+ ):
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
+ )
698
+
699
+
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
+ )
712
+
713
+ else:
714
+
715
+ class Http2Transport(BaseHttpTransport): # type: ignore
716
+ """The HTTP2 transport based on httpcore."""
717
+
718
+ TIMEOUT = 15
719
+
720
+ if TYPE_CHECKING:
721
+ _pool: Union[
722
+ httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool
723
+ ]
724
+
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)
839
+
190
840
 
191
841
  class _FunctionTransport(Transport):
192
- def __init__(self, func):
193
- # type: (Callable[[Dict[str, Any]], None]) -> None
842
+ """
843
+ DEPRECATED: Users wishing to provide a custom transport should subclass
844
+ the Transport class, rather than providing a function.
845
+ """
846
+
847
+ def __init__(
848
+ self,
849
+ func, # type: Callable[[Event], None]
850
+ ):
851
+ # type: (...) -> None
194
852
  Transport.__init__(self)
195
853
  self._func = func
196
854
 
197
- def capture_event(self, event):
198
- # type: (Dict[str, Any]) -> None
855
+ def capture_event(
856
+ self,
857
+ event, # type: Event
858
+ ):
859
+ # type: (...) -> None
199
860
  self._func(event)
200
861
  return None
201
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
+
202
871
 
203
872
  def make_transport(options):
204
- # type: (ClientOptions) -> Optional[Transport]
873
+ # type: (Dict[str, Any]) -> Optional[Transport]
205
874
  ref_transport = options["transport"]
206
875
 
207
- # If no transport is given, we use the http transport class
208
- if ref_transport is None:
209
- transport_cls = HttpTransport # type: Type[Transport]
210
- else:
211
- try:
212
- issubclass(ref_transport, type) # type: ignore
213
- except TypeError:
214
- # if we are not a class but we are a callable, assume a
215
- # function that acts as capture_event
216
- if callable(ref_transport):
217
- return _FunctionTransport(ref_transport)
218
- # otherwise assume an object fulfilling the transport contract
219
- return ref_transport
220
- transport_cls = ref_transport # type: ignore
221
-
222
- # if a transport class is given only instanciate it if the dsn is not
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):
882
+ return ref_transport
883
+ elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport):
884
+ transport_cls = ref_transport
885
+ elif callable(ref_transport):
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)
893
+
894
+ # if a transport class is given only instantiate it if the dsn is not
223
895
  # empty or None
224
896
  if options["dsn"]:
225
897
  return transport_cls(options)